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
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -759,7 +759,13 @@ frontend. For a module call the edge issues a Work Context for the person at the
module (`Issue` with the module as the audience and no scopes, so the token
carries what the person's roles grant there). It caches the context until
shortly before it expires. It strips `Authorization`, `Cookie` and every
`x-codefly-*` header the browser sent, then sets `x-codefly-work-context`. The
`x-codefly-*` header the browser sent, then sets `x-codefly-work-context` and a
fresh `x-codefly-effect-id` (`receipts.EffectIDHeaderName`) for the call, dropping
any `Idempotency-Key`, the SDK's other spelling of the effect id. An
operation that keeps receipts (Codefly's `sdk-go/receipts`) requires an effect id
on every call; the edge names each one so a browser never can, and so can never
replay another person's receipt. Each module call is its own effect: a browser
retry is a new attempt. The
module's faces dial the hop, which checks the method. A write authenticated by
the cookie must carry this origin in `Origin`. Each refusal names its reason.

Expand Down
21 changes: 17 additions & 4 deletions modules/authority/services/edge/code/edge.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import (
installationv1 "github.com/codefly-dev/authority/libraries/authority-client/go/gen/host/installation/v1"
sessionv1 "github.com/codefly-dev/authority/libraries/authority-client/go/gen/host/session/v1"
"github.com/codefly-dev/authority/libraries/authority-client/go/host"
"github.com/codefly-dev/sdk-go/receipts"
"github.com/google/uuid"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
Expand Down Expand Up @@ -151,7 +153,7 @@ func (e *Edge) ServeHTTP(w http.ResponseWriter, r *http.Request) {
refuse(w, http.StatusNotFound, "no module serves "+r.URL.Path)
return
}
proxy(e.Frontend, "", w, r)
proxy(e.Frontend, "", "", w, r)
}

// route is the module a path is for and the face that serves it.
Expand Down Expand Up @@ -197,7 +199,9 @@ func (e *Edge) toModule(w http.ResponseWriter, r *http.Request, module Module, t
refuseCall(w, r, code, "the authority refused this person at "+module.Name+": "+status.Convert(err).Message())
return
}
proxy(target, wc.token, w, r)
// Each module call is its own effect, minted here: an operation that keeps
// receipts requires an effect id, and a browser may not name one.
proxy(target, wc.token, uuid.NewString(), w, r)
}

// workContext is the person's Work Context at module, issued by the authority
Expand Down Expand Up @@ -450,8 +454,11 @@ func sameOrigin(r *http.Request) bool {

// proxy sends r on to target, with the Work Context the edge holds for it and
// nothing a caller said about who it is: no Authorization, no Cookie and no
// x-codefly-* header leaves the edge unless the edge set it.
func proxy(target *url.URL, token string, w http.ResponseWriter, r *http.Request) {
// x-codefly-* header leaves the edge unless the edge set it. effect, when set,
// is the effect id the call is attempted under: the edge names every module
// call's effect, so a caller can never name one and replay another person's
// receipt.
func proxy(target *url.URL, token, effect string, w http.ResponseWriter, r *http.Request) {
(&httputil.ReverseProxy{
// Never through a proxy from the environment, which would read the
// person's Work Context.
Expand All @@ -469,6 +476,12 @@ func proxy(target *url.URL, token string, w http.ResponseWriter, r *http.Request
if token != "" {
pr.Out.Header.Set(WorkContextKey, token)
}
if effect != "" {
// Idempotency-Key is the SDK's HTTP spelling of the effect id;
// a caller's would be a second effect id beside the minted one.
pr.Out.Header.Del(receipts.IdempotencyKeyHeaderName)
pr.Out.Header.Set(receipts.EffectIDHeaderName, effect)
}
},
ErrorHandler: func(w http.ResponseWriter, _ *http.Request, err error) {
refuse(w, http.StatusBadGateway, "the module did not answer: "+err.Error())
Expand Down
51 changes: 51 additions & 0 deletions modules/authority/services/edge/code/effect_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package main

import (
"net/http"
"testing"

"github.com/codefly-dev/sdk-go/receipts"
"github.com/google/uuid"
)

// A module operation that keeps receipts requires an effect id on every call,
// and the edge forwards no x-codefly-* header a browser sent. So the edge
// mints one for each module call: a browser can't name an effect, and so can
// never replay another person's receipt, yet its call still reaches a guarded
// operation.
func TestEachModuleCallCarriesAnEffectIDTheEdgeMinted(t *testing.T) {
f := newFixture(t)
cookie := f.signIn(t)
minted := func(seen http.Header) string {
t.Helper()
id := seen.Get(receipts.EffectIDHeaderName)
if _, err := uuid.Parse(id); err != nil {
t.Fatalf("the module received effect id %q: %v", id, err)
}
return id
}
// Idempotency-Key is the SDK's HTTP spelling of the same effect id: left in
// place beside the minted one, it would be a second effect id, which the SDK
// refuses before running the operation.
if res, _ := f.do(t, http.MethodGet, "/v1/records/types", http.Header{"Cookie": {cookie.String()}, "X-Codefly-Effect-Id": {"forged"}, receipts.IdempotencyKeyHeaderName: {"forged"}}); res.StatusCode != http.StatusOK {
t.Fatalf("answered %d", res.StatusCode)
}
seen, _ := f.rest.last()
first := minted(seen)
if first == "forged" || seen.Get(receipts.IdempotencyKeyHeaderName) != "" {
t.Fatalf("the browser named the effect: %v", seen)
}
if res, _ := f.do(t, http.MethodGet, "/v1/records/types", http.Header{"Cookie": {cookie.String()}}); res.StatusCode != http.StatusOK {
t.Fatalf("answered %d", res.StatusCode)
}
if seen, _ := f.rest.last(); minted(seen) == first {
t.Fatal("two calls were minted one effect")
}
// The Connect face, by the service its path names.
if res, _ := f.do(t, http.MethodPost, "/records.v1.RecordsService/Query", http.Header{"Cookie": {cookie.String()}, "Origin": {f.edge.URL}, receipts.IdempotencyKeyHeaderName: {"forged"}}); res.StatusCode != http.StatusOK {
t.Fatalf("a Connect call answered %d", res.StatusCode)
}
if seen, _ := f.connect.last(); minted(seen) == first || seen.Get(receipts.IdempotencyKeyHeaderName) != "" {
t.Fatalf("the Connect call reused an effect or kept the caller's: %v", seen)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func TestTheEdgeNeverForwardsThePersonsWorkContextThroughAnEnvironmentProxy(t *t
ctx, cancel := context.WithTimeout(context.Background(), 300*time.Millisecond)
defer cancel()
before := proxied.Load()
proxy(target, "SECRET-WC", httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/v1/documents", nil).WithContext(ctx))
proxy(target, "SECRET-WC", "", httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/v1/documents", nil).WithContext(ctx))
if n := proxied.Load() - before; n != 0 {
t.Fatalf("the edge forwarded the Work Context through the environment proxy (%d connections)", n)
}
Expand Down
1 change: 1 addition & 0 deletions modules/authority/services/edge/code/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ require (
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/oklog/ulid/v2 v2.1.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 // indirect
github.com/yoheimuta/go-protoparser/v4 v4.14.2 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect
golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba // indirect
Expand Down
12 changes: 12 additions & 0 deletions modules/authority/services/edge/code/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=
github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.8.2 h1:9d9DEokx5W7qlr0shkFXIxEA7+TTD+llwzcykSlxMNc=
Expand Down Expand Up @@ -129,6 +131,14 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0=
github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e h1:Q6MvJtQK/iRcRtzAscm/zF23XxJlbECiGPyRicsX+Ak=
github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
Expand Down Expand Up @@ -198,6 +208,8 @@ golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba h1:Ck8QetSgk912qxWLMCKxd0in+
golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba/go.mod h1:50RgIsmK7OwqzTTeqcSXQW8SswW0o8fRcDxmqGluJ8E=
golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU=
golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
Expand Down
Loading