mirror of
https://github.com/hanzoai/base.git
synced 2026-10-21 03:41:50 +00:00
An adapted net/http handler was handed a request with an empty RequestURI, so anything that routes or logs on the target saw "/" and answered for a path nobody asked for. The pin carries the fix. The registrations in the tests say which method they answer with, the same as the ones they exercise. Co-authored-by: Hanzo Dev <[email protected]>
134 lines
4.0 KiB
Go
134 lines
4.0 KiB
Go
package org
|
|
|
|
import (
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/zap-proto/zip"
|
|
)
|
|
|
|
// serveIAM stands up a zip app on a unix socket with no scheme, which is ZAP —
|
|
// the same wire Hanzo IAM's own `serve --zap` listens on. It answers the two
|
|
// routes IAMClient reads, and records the Authorization header it saw so a test
|
|
// can prove the credential crossed the wire rather than merely that bytes did.
|
|
func serveIAM(t *testing.T) (addr string, seen *string) {
|
|
t.Helper()
|
|
|
|
var got string
|
|
app := zip.New(zip.Config{AppName: "iam-fake", DisableStartupMessage: true})
|
|
app.Raw(http.MethodGet, "/v1/iam/oauth/userinfo", func(c *zip.Ctx) error {
|
|
got = c.Header("Authorization")
|
|
return c.JSON(200, map[string]any{
|
|
"id": "u-1",
|
|
"email": "[email protected]",
|
|
"name": "alice",
|
|
"orgIds": []string{"hanzo"},
|
|
})
|
|
})
|
|
app.Raw(http.MethodGet, "/v1/iam/keys/principal", func(c *zip.Ctx) error {
|
|
if c.Query("accessKey") != "sk-probe" {
|
|
return c.JSON(400, map[string]string{"status": "error", "msg": "the entity does not exist"})
|
|
}
|
|
return c.JSON(200, map[string]any{
|
|
"status": "ok",
|
|
"data": map[string]any{
|
|
"name": "svc", "email": "[email protected]", "owner": "hanzo", "isAdmin": false,
|
|
},
|
|
})
|
|
})
|
|
|
|
// A socket rather than a port: no allocation race, nothing left listening on
|
|
// a shared address if a test fails.
|
|
addr = filepath.Join(t.TempDir(), "iam.sock")
|
|
if _, err := zip.Serve(app, addr); err != nil {
|
|
t.Fatalf("serve: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = app.Shutdown() })
|
|
|
|
// Serve returns once the listeners are STARTING; the socket appears a moment
|
|
// later. Wait for the address to exist rather than race it.
|
|
deadline := time.Now().Add(5 * time.Second)
|
|
for {
|
|
if _, err := os.Stat(addr); err == nil {
|
|
break
|
|
}
|
|
if time.Now().After(deadline) {
|
|
t.Fatalf("zap listener never bound %s", addr)
|
|
}
|
|
time.Sleep(2 * time.Millisecond)
|
|
}
|
|
|
|
return addr, &got
|
|
}
|
|
|
|
// A bare address selects ZAP, and every read IAMClient makes crosses it intact.
|
|
func TestIAMClientOverZAP(t *testing.T) {
|
|
addr, seen := serveIAM(t)
|
|
|
|
c := NewIAMClient(addr)
|
|
|
|
user, err := c.ValidateToken("tok-abc")
|
|
if err != nil {
|
|
t.Fatalf("ValidateToken over zap: %v", err)
|
|
}
|
|
if user.ID != "u-1" || user.Email != "[email protected]" {
|
|
t.Fatalf("decoded %+v", user)
|
|
}
|
|
if *seen != "Bearer tok-abc" {
|
|
t.Fatalf("authorization did not cross the wire: %q", *seen)
|
|
}
|
|
|
|
c.SetAdminCreds(AdminCreds{ClientID: "svc", ClientSecret: "shh", Owner: "hanzo"})
|
|
key, err := c.ResolveAPIKey("sk-probe")
|
|
if err != nil {
|
|
t.Fatalf("ResolveAPIKey over zap: %v", err)
|
|
}
|
|
if key.ID != "hanzo/svc" || len(key.OrgIDs) != 1 || key.OrgIDs[0] != "hanzo" {
|
|
t.Fatalf("decoded %+v", key)
|
|
}
|
|
}
|
|
|
|
// The endpoint's scheme is the only thing that selects the wire, and an http
|
|
// endpoint keeps net/http exactly as before.
|
|
func TestResolveIAMSelectsWireByScheme(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
endpoint string
|
|
wantURL string
|
|
wantZAP bool
|
|
}{
|
|
{"", "https://hanzo.id", false},
|
|
{"https://hanzo.id/", "https://hanzo.id", false},
|
|
{"http://iam.hanzo.svc:8000", "http://iam.hanzo.svc:8000", false},
|
|
{"iam.hanzo.svc:9653", "http://iam.hanzo.svc:9653", true},
|
|
{"zap://iam.hanzo.svc:9653", "http://iam.hanzo.svc:9653", true},
|
|
{"/run/hanzo/iam.sock", "http://iam", true},
|
|
} {
|
|
url, client := resolveIAM(tc.endpoint, 0)
|
|
if url != tc.wantURL {
|
|
t.Errorf("%q -> url %q, want %q", tc.endpoint, url, tc.wantURL)
|
|
}
|
|
_, isZAP := client.Transport.(*zapTransport)
|
|
if isZAP != tc.wantZAP {
|
|
t.Errorf("%q -> zap %v, want %v", tc.endpoint, isZAP, tc.wantZAP)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A token is minted against the brand, whose host decides the issuer, so the
|
|
// in-cluster address is refused rather than quietly stamping the wrong brand.
|
|
func TestExchangeRefusesClusterAddress(t *testing.T) {
|
|
_, _, err := ExchangeOAuth2Token("code", "https://app/cb", Config{
|
|
IAMEndpoint: "iam.hanzo.svc:9653",
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected a refusal for a non-http endpoint")
|
|
}
|
|
if !strings.Contains(err.Error(), "brand") {
|
|
t.Fatalf("unhelpful error: %v", err)
|
|
}
|
|
}
|