quorate/packages/grafana-rcon/backend/auth_test.go

158 lines
5 KiB
Go
Raw Permalink Normal View History

package main
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"sync/atomic"
"testing"
"time"
"github.com/go-jose/go-jose/v4"
"github.com/go-jose/go-jose/v4/jwt"
"github.com/grafana/grafana-plugin-sdk-go/config"
)
type authFixture struct {
key *ecdsa.PrivateKey
server *httptest.Server
admin atomic.Bool
status atomic.Int32
badJSON atomic.Bool
keyCalls atomic.Int32
userCalls atomic.Int32
bearer atomic.Value
}
func newAuthFixture(t *testing.T) *authFixture {
t.Helper()
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
f := &authFixture{key: key}
f.admin.Store(true)
f.status.Store(http.StatusOK)
f.server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
switch req.URL.Path {
case "/api/signing-keys/keys":
f.keyCalls.Add(1)
_ = json.NewEncoder(w).Encode(jose.JSONWebKeySet{Keys: []jose.JSONWebKey{{Key: &key.PublicKey, KeyID: "synthetic"}}})
case "/api/users/17":
f.userCalls.Add(1)
f.bearer.Store(req.Header.Get("Authorization"))
w.WriteHeader(int(f.status.Load()))
if f.badJSON.Load() {
_, _ = w.Write([]byte("invalid JSON"))
return
}
_ = json.NewEncoder(w).Encode(map[string]any{"isGrafanaAdmin": f.admin.Load(), "role": "Admin"})
default:
http.NotFound(w, req)
}
}))
t.Cleanup(f.server.Close)
return f
}
func signedID(t *testing.T, key *ecdsa.PrivateKey, subject string, expires time.Time) string {
t.Helper()
signer, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jose.JSONWebKey{Key: key, KeyID: "synthetic"}}, new(jose.SignerOptions).WithType("jwt"))
if err != nil {
t.Fatal(err)
}
token, err := jwt.Signed(signer).Claims(jwt.Claims{Subject: subject, Expiry: jwt.NewNumericDate(expires)}).Serialize()
if err != nil {
t.Fatal(err)
}
return token
}
func authRequest(identity, secret string) *http.Request {
ctx := config.WithGrafanaConfig(context.Background(), config.NewGrafanaCfg(map[string]string{config.AppClientSecret: secret}))
req := httptest.NewRequest("POST", "/command", strings.NewReader(`{"command":"list"}`)).WithContext(ctx)
req.Header.Set("X-Grafana-Id", identity)
req.Header.Set("X-Grafana-User", "forged-admin")
req.Header.Set("X-Grafana-Role", "Admin")
return req
}
func TestRoleRevocation(t *testing.T) {
f := newAuthFixture(t)
auth := newAuthorizer(f.server.URL)
identity := signedID(t, f.key, "user:17", time.Now().Add(time.Minute))
calls := 0
c := &console{allowed: auth.canSubmit, execute: func(context.Context, string) (string, error) { calls++; return "ok", nil }}
for _, step := range []struct {
admin bool
secret string
status int
}{
{true, "synthetic-first", 200},
{false, "synthetic-second", 403},
{false, "synthetic-second", 403},
{true, "synthetic-second", 200},
} {
f.admin.Store(step.admin)
w := httptest.NewRecorder()
c.routes().ServeHTTP(w, authRequest(identity, step.secret))
if w.Code != step.status {
t.Fatalf("status=%d want=%d", w.Code, step.status)
}
}
if calls != 2 || f.keyCalls.Load() != 1 || f.userCalls.Load() != 4 || f.bearer.Load() != "Bearer synthetic-second" {
t.Fatalf("commands=%d keys=%d users=%d bearer=%v", calls, f.keyCalls.Load(), f.userCalls.Load(), f.bearer.Load())
}
}
func TestIdentityFailures(t *testing.T) {
for _, name := range []string{"missing ID", "forged ID", "expired ID", "wrong signature", "service account", "empty user", "missing secret", "user API failure", "invalid profile", "organization admin"} {
t.Run(name, func(t *testing.T) {
f := newAuthFixture(t)
identity := signedID(t, f.key, "user:17", time.Now().Add(time.Minute))
secret := "synthetic-service"
status := http.StatusServiceUnavailable
switch name {
case "missing ID":
identity = ""
case "forged ID":
identity = "admin"
case "expired ID":
identity = signedID(t, f.key, "user:17", time.Now().Add(-time.Hour))
case "wrong signature":
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
identity = signedID(t, key, "user:17", time.Now().Add(time.Minute))
case "service account":
identity = signedID(t, f.key, "service-account:17", time.Now().Add(time.Minute))
status = http.StatusForbidden
case "empty user":
identity = signedID(t, f.key, "user:", time.Now().Add(time.Minute))
status = http.StatusForbidden
case "missing secret":
secret = ""
case "user API failure":
f.status.Store(http.StatusInternalServerError)
case "invalid profile":
f.badJSON.Store(true)
case "organization admin":
f.admin.Store(false)
status = http.StatusForbidden
}
calls := 0
c := &console{allowed: newAuthorizer(f.server.URL).canSubmit, execute: func(context.Context, string) (string, error) { calls++; return "ok", nil }}
w := httptest.NewRecorder()
c.routes().ServeHTTP(w, authRequest(identity, secret))
if w.Code != status || calls != 0 {
t.Fatalf("status=%d want=%d commands=%d", w.Code, status, calls)
}
})
}
}