[debug] Sign with SigV4 and print the vendor's refusal

Two things the first pass could not do.

An environment may carry SigV4 credentials rather than a Bedrock API key,
and skipping there wastes the only account within reach. It now signs with
the default credential chain when no bearer token is set, and records which
mode it used. Fetch is bearer-only, so its step is skipped under SigV4 —
itself worth knowing, since it means a record holding an access key cannot
discover at all.

A non-200 printed nothing but its status. The body is the whole point of a
failure here: an IAM denial names the action it refused, which is a
different fix from a credential AWS does not recognise.
This commit is contained in:
mlsmaycon
2026-08-23 16:25:22 +00:00
parent 36cd7a5915
commit 011e96f0fe

View File

@@ -16,6 +16,8 @@ package modeldiscovery
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"io"
"net/http"
@@ -26,6 +28,8 @@ import (
"testing"
"time"
v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
awsconfig "github.com/aws/aws-sdk-go-v2/config"
"github.com/stretchr/testify/require"
"github.com/netbirdio/netbird/management/internals/modules/agentnetwork/catalog"
@@ -59,8 +63,8 @@ import (
// and the filter is the thing to look at
func TestDebugBedrockProfileCount(t *testing.T) {
token := os.Getenv("AWS_BEARER_TOKEN_BEDROCK")
if token == "" {
t.Skip("AWS_BEARER_TOKEN_BEDROCK not set; export it to run the Bedrock count debug")
if token == "" && os.Getenv("AWS_ACCESS_KEY_ID") == "" {
t.Skip("no Bedrock credential; export AWS_BEARER_TOKEN_BEDROCK or AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY")
}
region := os.Getenv("AWS_REGION")
if region == "" {
@@ -70,7 +74,11 @@ func TestDebugBedrockProfileCount(t *testing.T) {
defer cancel()
host := "bedrock." + region + ".amazonaws.com"
t.Logf("=== region %s, control plane %s ===", region, host)
authMode := "bearer token"
if token == "" {
authMode = "SigV4 (access key)"
}
t.Logf("=== region %s, control plane %s, auth %s ===", region, host, authMode)
// [1] Exactly what Fetch asks for: no maxResults, no type filter.
first, firstRaw := listProfiles(t, ctx, host, token, nil)
@@ -91,18 +99,23 @@ func TestDebugBedrockProfileCount(t *testing.T) {
}
// [3] The path the Load models button drives, including its ACTIVE filter
// and its dedup.
var cl Client
fetched, err := cl.Fetch(ctx, Request{
CatalogID: "bedrock_api",
UpstreamURL: "https://bedrock-runtime." + region + ".amazonaws.com",
APIKey: token,
})
require.NoError(t, err, "Fetch must reach the control plane")
t.Logf("[3] Fetch returned %d models (this is what the dashboard renders)", len(fetched))
if len(first.Summaries) == len(all) && len(fetched) > len(all) {
t.Logf(" NOTE: Fetch returned more than the raw listing — the surplus is ours, not AWS's")
// and its dedup. Fetch only knows how to send a bearer token, so with
// SigV4 credentials this step is not available — worth recording, because
// it means a record holding an access key cannot discover at all.
if token == "" {
t.Logf("[3] skipped: Fetch sends a bearer token only, and this environment has SigV4 credentials")
} else {
var cl Client
fetched, err := cl.Fetch(ctx, Request{
CatalogID: "bedrock_api",
UpstreamURL: "https://bedrock-runtime." + region + ".amazonaws.com",
APIKey: token,
})
require.NoError(t, err, "Fetch must reach the control plane")
t.Logf("[3] Fetch returned %d models (this is what the dashboard renders)", len(fetched))
if len(first.Summaries) == len(all) && len(fetched) > len(all) {
t.Logf(" NOTE: Fetch returned more than the raw listing — the surplus is ours, not AWS's")
}
}
// Decomposition of everything AWS returned.
@@ -203,8 +216,15 @@ func listProfiles(t *testing.T, ctx context.Context, host, token string, query m
req, err := http.NewRequestWithContext(ctx, http.MethodGet, target.String(), nil)
require.NoError(t, err)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Accept", "application/json")
if token != "" {
req.Header.Set("Authorization", "Bearer "+token)
} else {
// The environment may only carry SigV4 credentials. Sign rather than
// skip: the count question is about what the account holds, and a
// bearer token is not the only way to ask.
signRequestSigV4(t, ctx, req, hostRegion(host))
}
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err, "reach the Bedrock control plane")
@@ -212,8 +232,13 @@ func listProfiles(t *testing.T, ctx context.Context, host, token string, query m
raw, err := io.ReadAll(io.LimitReader(resp.Body, 8<<20))
require.NoError(t, err)
require.Equalf(t, http.StatusOK, resp.StatusCode,
"control plane must answer the listing; got %d with %d bytes", resp.StatusCode, len(raw))
if resp.StatusCode != http.StatusOK {
// The body is the whole point of a failure here: an IAM denial names
// the action it refused, which is a different fix from a bad token.
t.Logf("control plane answered %d: %s", resp.StatusCode, strings.TrimSpace(string(raw)))
t.Logf(" x-amzn-errortype: %s", resp.Header.Get("x-amzn-errortype"))
}
require.Equal(t, http.StatusOK, resp.StatusCode, "control plane must answer the listing")
var page profilePage
require.NoError(t, json.Unmarshal(raw, &page), "listing must parse")
@@ -257,3 +282,28 @@ func logCounts(t *testing.T, label string, counts map[string]int) {
t.Logf(" %-30s %d", k, counts[k])
}
}
// hostRegion pulls the region back out of a control-plane host.
func hostRegion(host string) string {
parts := strings.Split(host, ".")
if len(parts) < 2 {
return ""
}
return parts[1]
}
// signRequestSigV4 signs req for the Bedrock control plane with whatever
// credentials the default chain resolves.
func signRequestSigV4(t *testing.T, ctx context.Context, req *http.Request, region string) {
t.Helper()
cfg, err := awsconfig.LoadDefaultConfig(ctx, awsconfig.WithRegion(region))
require.NoError(t, err, "load AWS config")
creds, err := cfg.Credentials.Retrieve(ctx)
require.NoError(t, err, "resolve AWS credentials")
empty := sha256.Sum256(nil)
require.NoError(t,
v4.NewSigner().SignHTTP(ctx, creds, req, hex.EncodeToString(empty[:]), "bedrock", region, time.Now()),
"sign the request")
}