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
19 changes: 14 additions & 5 deletions internal/proxy/multitenant.go
Original file line number Diff line number Diff line change
Expand Up @@ -497,17 +497,17 @@ func (m *MultiTenant) handleStackAuth(w http.ResponseWriter, r *http.Request) {
return
}
}
capabilities, err := stackTenantCapabilities(input.Capabilities)
if err != nil {
http.Error(w, "tenant capabilities are invalid", http.StatusBadRequest)
return
}
controlToken := strings.TrimSpace(r.Header.Get("X-Subrouter-Stack-Control-Token"))
if len(m.StackTenantDeleteToken) < 32 ||
subtle.ConstantTimeCompare([]byte(controlToken), m.StackTenantDeleteToken) != 1 {
http.Error(w, "trusted service credential required", http.StatusUnauthorized)
return
}
capabilities, err := stackTenantCapabilities(input.Capabilities)
if err != nil {
http.Error(w, "tenant capabilities are invalid", http.StatusBadRequest)
return
}
base := strings.TrimRight(strings.TrimSpace(m.PublicURL), "/")
if base == "" {
http.Error(w, "hosted proxy URL is not configured", http.StatusInternalServerError)
Expand Down Expand Up @@ -553,6 +553,15 @@ func (m *MultiTenant) handleStackAuth(w http.ResponseWriter, r *http.Request) {
}

func stackTenantCapabilities(raw []string) ([]tenant.Capability, error) {
// The first trusted cmux.com exchange broker predates capability-scoped
// tenant keys. Preserve that authenticated service boundary during the
// rollout; untrusted direct callers are rejected before this fallback.
if len(raw) == 0 {
return []tenant.Capability{
tenant.CapabilityManageAccounts,
tenant.CapabilityUse,
}, nil
}
seen := map[tenant.Capability]bool{}
capabilities := make([]tenant.Capability, 0, len(raw))
for _, value := range raw {
Expand Down
15 changes: 11 additions & 4 deletions internal/proxy/multitenant_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"net/url"
"os"
"path/filepath"
"reflect"
"sort"
"strings"
"sync/atomic"
Expand Down Expand Up @@ -524,7 +525,7 @@ func TestStackLoginCreatesStableTenantAndAcceptsDirectAccountUpload(t *testing.T
req := httptest.NewRequest(
http.MethodPost,
"/_subrouter/auth/stack",
strings.NewReader(`{"teamId":"team-123","teamName":"Acme","capabilities":["manage_accounts"]}`),
strings.NewReader(`{"teamId":"team-123","teamName":"Acme"}`),
)
req.Header.Set("Authorization", "Bearer stack-access")
req.Header.Set("X-Subrouter-Stack-Control-Token", testStackTenantDeleteToken)
Expand Down Expand Up @@ -554,6 +555,12 @@ func TestStackLoginCreatesStableTenantAndAcceptsDirectAccountUpload(t *testing.T
if first["proxyUrl"] != "https://sr.example/t/"+key {
t.Fatalf("proxy URL = %v", first["proxyUrl"])
}
if got := first["capabilities"]; !reflect.DeepEqual(
got,
[]any{"manage_accounts", "use"},
) {
t.Fatalf("capabilities = %#v", got)
}

upload := httptest.NewRequest(
http.MethodPost,
Expand Down Expand Up @@ -697,9 +704,9 @@ func TestLegacyDirectStackCredentialExpiresAfterDeploymentGrace(t *testing.T) {
handler.ServeHTTP(
response,
httptest.NewRequest(
http.MethodGet,
"/t/"+legacyKey+"/_subrouter/whoami",
nil,
http.MethodGet,
"/t/"+legacyKey+"/_subrouter/whoami",
nil,
),
)
return response
Expand Down