Skip to content
Open
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
7 changes: 6 additions & 1 deletion module/services/auth-gateway/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,12 @@ instance reloads; the owner's prepublication overlap requirement still applies.
(ext_authz, revocation included), the call is metered on the organization's
budget, the authority's `Issue` gives the person's Work Context at the module
whose published routes resolve the path (held until shortly before it
lapses), and the call goes through the same HTTP face in-process with that
lapses). An authority Check refusal evicts only that same cached context;
the response is returned without replay, and a later explicit call issues
again. A downstream refusal after Check does not evict; a later Check must
observe stale authority first. Recovery is per-instance best effort, not a
cross-replica or next-request guarantee.
The call goes through the same HTTP face in-process with that
context and nothing else: no `Authorization`, no `Cookie`, no identity header,
no caller `x-codefly-*` header. A path published by two modules is refused
(409); a module route under a `/v1/` prefix the catalog owns is refused (403).
Expand Down
38 changes: 35 additions & 3 deletions module/services/auth-gateway/code/gateway_hop.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,9 @@ func (g *Gateway) serveBrowser(w http.ResponseWriter, r *http.Request) bool {
hopBrowserError(w, r, code, status.Code(err), "the authority refused this person at "+module+": "+status.Convert(err).Message())
return
}
h.face.ServeHTTP(w, withOnlyWorkContext(r, token))
key := sha256.Sum256([]byte(bearer + "\x00" + module))
marked := r.WithContext(context.WithValue(r.Context(), browserCheckKey{}, browserCheck{key: key, token: token, audience: module}))
h.face.ServeHTTP(w, withOnlyWorkContext(marked, token))
}))
return true
}
Expand Down Expand Up @@ -367,6 +369,34 @@ type hopAuthorityClient interface {
hopIssuer
}

// browserCheck stays in the request context and never crosses the hop boundary.
type browserCheckKey struct{}
type browserCheck struct {
key [32]byte
token, audience string
}

// browserCheckAuthority observes the hop's own live authority decision without
// changing it or guessing from the downstream protocol's response.
type browserCheckAuthority struct {
hopAuthorityClient
role *hopRole
}

func (a browserCheckAuthority) Check(ctx context.Context, req *authorityv1.CheckRequest, opts ...grpc.CallOption) (*authorityv1.CheckResponse, error) {
out, err := a.hopAuthorityClient.Check(ctx, req, opts...)
marker, ok := ctx.Value(browserCheckKey{}).(browserCheck)
denied := status.Code(err) == codes.PermissionDenied || status.Code(err) == codes.Unauthenticated || (err == nil && out != nil && out.GetRefusal() != "")
if ok && denied && ctx.Err() == nil && req.GetToken() == marker.token && req.GetAudience() == marker.audience {
a.role.mu.Lock()
if held, exists := a.role.held[marker.key]; exists && held.token == marker.token {
delete(a.role.held, marker.key)
}
a.role.mu.Unlock()
}
return out, err
}

// hopAuthority is the interface's generated client, as hopAuthorityClient.
type hopAuthority struct {
authorityv1.AuthorityServiceClient
Expand All @@ -381,15 +411,17 @@ func newHopRole(ctx context.Context, authority hopAuthorityClient, credential fu
if err != nil {
return nil, nil, err
}
role := &hopRole{modules: modules, routes: routes, issuer: authority}
front, err := forward.New(ctx, forward.Config{
Signer: signer, Authority: authority, Credential: credential,
Signer: signer, Authority: browserCheckAuthority{hopAuthorityClient: authority, role: role}, Credential: credential,
Modules: modules, Routes: raw, Enforcement: enforcement, Log: wool.Get(ctx).In("hop"),
})
if err != nil {
return nil, nil, err
}
log.Printf("hop: standing in front of %s", strings.Join(modules, ", "))
return &hopRole{modules: modules, routes: routes, issuer: authority, face: front.Handler, front: front}, front, nil
role.face, role.front = front.Handler, front
return role, front, nil
}

// hopFace is the hop's one face, as the hop endpoint serves it: a gRPC call
Expand Down
Loading
Loading