Feature(Go): issue-token subcommand for minting customer JWTs

This commit is contained in:
yuanyuanxiang
2026-06-04 10:04:02 +02:00
parent 4064bbe25d
commit be09b271e1
7 changed files with 328 additions and 36 deletions

View File

@@ -138,7 +138,7 @@ func TestLocalSignerDeterministic(t *testing.T) {
func TestRemoteSignerCacheHit(t *testing.T) {
priv := testKey(t)
master := mustLocal(t, "real-hmac-key-for-test-xx")
ls := NewLicenseServer(master, &priv.PublicKey, time.Minute, silentLogger{})
ls := NewLicenseServer(master, &priv.PublicKey, time.Minute, silentLogger{}, "")
ts := httptest.NewServer(ls.Handler())
defer ts.Close()
@@ -180,7 +180,7 @@ func TestRemoteSignerCacheHit(t *testing.T) {
func TestRemoteSignerStaleFallback(t *testing.T) {
priv := testKey(t)
master := mustLocal(t, "master-fallback-test-xxx")
ls := NewLicenseServer(master, &priv.PublicKey, time.Minute, silentLogger{})
ls := NewLicenseServer(master, &priv.PublicKey, time.Minute, silentLogger{}, "")
ts := httptest.NewServer(ls.Handler())
tok, err := Issue(priv, "cust-fallback", TierPaid, 5, time.Hour)
@@ -214,7 +214,7 @@ func TestRemoteSignerStaleFallback(t *testing.T) {
func TestQuotaEnforcement(t *testing.T) {
priv := testKey(t)
master := mustLocal(t, "master-quota-test-xxxxxx")
ls := NewLicenseServer(master, &priv.PublicKey, time.Minute, silentLogger{})
ls := NewLicenseServer(master, &priv.PublicKey, time.Minute, silentLogger{}, "")
ts := httptest.NewServer(ls.Handler())
defer ts.Close()
@@ -260,7 +260,7 @@ func TestQuotaEnforcement(t *testing.T) {
func TestAnonymousTrialSignsAndCaps(t *testing.T) {
priv := testKey(t)
master := mustLocal(t, "master-trial-test-xxxxxx")
ls := NewLicenseServer(master, &priv.PublicKey, time.Minute, silentLogger{})
ls := NewLicenseServer(master, &priv.PublicKey, time.Minute, silentLogger{}, "")
ts := httptest.NewServer(ls.Handler())
defer ts.Close()
@@ -303,7 +303,7 @@ func TestAnonymousTrialSignsAndCaps(t *testing.T) {
func TestAnonymousTrialIPRateLimit(t *testing.T) {
priv := testKey(t)
master := mustLocal(t, "master-rate-test-xxxxxxx")
ls := NewLicenseServer(master, &priv.PublicKey, time.Minute, silentLogger{})
ls := NewLicenseServer(master, &priv.PublicKey, time.Minute, silentLogger{}, "")
ts := httptest.NewServer(ls.Handler())
defer ts.Close()
@@ -335,7 +335,7 @@ func TestAnonymousTrialIPRateLimit(t *testing.T) {
func TestAuthRejectsBadBearer(t *testing.T) {
priv := testKey(t)
master := mustLocal(t, "master-bad-bearer-xxxxxx")
ls := NewLicenseServer(master, &priv.PublicKey, time.Minute, silentLogger{})
ls := NewLicenseServer(master, &priv.PublicKey, time.Minute, silentLogger{}, "")
ts := httptest.NewServer(ls.Handler())
defer ts.Close()
@@ -378,7 +378,7 @@ func TestRemoteSignerHardFailNoCacheReturnsError(t *testing.T) {
func TestHeartbeatRefreshOnly(t *testing.T) {
priv := testKey(t)
master := mustLocal(t, "master-hb-test-xxxxxxxxxx")
ls := NewLicenseServer(master, &priv.PublicKey, time.Minute, silentLogger{})
ls := NewLicenseServer(master, &priv.PublicKey, time.Minute, silentLogger{}, "")
ts := httptest.NewServer(ls.Handler())
defer ts.Close()
@@ -461,7 +461,7 @@ func TestHeartbeatRefreshOnly(t *testing.T) {
func TestQuotaRejectionDoesNotConsumeSlot(t *testing.T) {
priv := testKey(t)
master := mustLocal(t, "master-no-leak-xxxxxxxxxxxx")
ls := NewLicenseServer(master, &priv.PublicKey, time.Minute, silentLogger{})
ls := NewLicenseServer(master, &priv.PublicKey, time.Minute, silentLogger{}, "")
ts := httptest.NewServer(ls.Handler())
defer ts.Close()
@@ -597,3 +597,36 @@ func TestJWTAlgLockedToRS256(t *testing.T) {
t.Error("VerifyJWT accepted RS384; alg should be locked to RS256")
}
}
// TestQuotaTrackerPersistence: after a simulated restart (new tracker loaded
// from the file written by the first), previously-admitted devices re-occupy
// their slots and a new over-quota device is still rejected.
func TestQuotaTrackerPersistence(t *testing.T) {
path := t.TempDir() + "/quota.json"
// First "run": admit dev-1 and dev-2 up to cap=2.
q1 := newQuotaTracker(5 * time.Minute)
q1.statePath = path
if _, ok := q1.Reserve("sub", "dev-1", 2); !ok {
t.Fatal("dev-1 should be admitted")
}
if _, ok := q1.Reserve("sub", "dev-2", 2); !ok {
t.Fatal("dev-2 should be admitted")
}
// Simulate restart: new tracker loads the persisted file.
q2 := newQuotaTracker(5 * time.Minute)
q2.statePath = path
if err := q2.Load(); err != nil {
t.Fatalf("Load: %v", err)
}
// Restored tracker knows about dev-1 and dev-2: quota full.
if count, ok := q2.Reserve("sub", "dev-3", 2); ok {
t.Errorf("dev-3 should be rejected after restore, count=%d", count)
}
// Existing devices re-sign successfully (idempotent refresh).
if _, ok := q2.Reserve("sub", "dev-1", 2); !ok {
t.Error("dev-1 re-sign should succeed after restore")
}
}