feat(auth): JWT(HS256 stdlib) + RBAC(owner/admin/viewer) + API key + 密码 hash;config 校验
This commit is contained in:
1 parent
7f7cb8fbf7
commit
a3f6f4f687
10 files changed
+1116
No files matched your search
@@ -0,0 +1,174 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func bearerHeader(token string) string {
|
||||
return "Bearer " + token
|
||||
}
|
||||
|
||||
func claimsHandler(t *testing.T) http.Handler {
|
||||
t.Helper()
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
claims, ok := ClaimsFromContext(r.Context())
|
||||
if !ok {
|
||||
http.Error(w, "no claims in context", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("X-Claims-Sub", claims.Sub)
|
||||
w.Header().Set("X-Claims-Role", string(claims.Role))
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
}
|
||||
|
||||
func doAuth(h http.Handler, tok string) *httptest.ResponseRecorder {
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
if tok != "" {
|
||||
req.Header.Set("Authorization", bearerHeader(tok))
|
||||
}
|
||||
rr := httptest.NewRecorder()
|
||||
h.ServeHTTP(rr, req)
|
||||
return rr
|
||||
}
|
||||
|
||||
func responseCode(t *testing.T, rr *httptest.ResponseRecorder, want int) {
|
||||
t.Helper()
|
||||
if rr.Code != want {
|
||||
t.Fatalf("status = %d, want %d (body: %s)", rr.Code, want, rr.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func assertErrorCode(t *testing.T, rr *httptest.ResponseRecorder, want string) {
|
||||
t.Helper()
|
||||
var body struct {
|
||||
Error struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
} `json:"error"`
|
||||
}
|
||||
if err := json.NewDecoder(strings.NewReader(rr.Body.String())).Decode(&body); err != nil {
|
||||
t.Fatalf("decode error body: %v", err)
|
||||
}
|
||||
if body.Error.Code != want {
|
||||
t.Errorf("error.code = %q, want %q", body.Error.Code, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthorize_NoBearer(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := New(testConfig(""))
|
||||
rr := doAuth(svc.Authorize(RoleViewer)(claimsHandler(t)), "")
|
||||
responseCode(t, rr, http.StatusUnauthorized)
|
||||
assertErrorCode(t, rr, "unauthorized")
|
||||
}
|
||||
|
||||
func TestAuthorize_InvalidToken(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := New(testConfig(""))
|
||||
rr := doAuth(svc.Authorize(RoleViewer)(claimsHandler(t)), "not-a-jwt")
|
||||
responseCode(t, rr, http.StatusUnauthorized)
|
||||
assertErrorCode(t, rr, "unauthorized")
|
||||
}
|
||||
|
||||
func TestAuthorize_ExpiredToken(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := New(testConfig(""))
|
||||
tok, err := svc.Sign("u", RoleViewer, -time.Minute)
|
||||
if err != nil {
|
||||
t.Fatalf("Sign error: %v", err)
|
||||
}
|
||||
rr := doAuth(svc.Authorize(RoleViewer)(claimsHandler(t)), tok)
|
||||
responseCode(t, rr, http.StatusUnauthorized)
|
||||
assertErrorCode(t, rr, "unauthorized")
|
||||
}
|
||||
|
||||
func TestAuthorize_OwnerSatisfiesAdminAndViewer(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := New(testConfig(""))
|
||||
tok, err := svc.Sign("u", RoleOwner, time.Hour)
|
||||
if err != nil {
|
||||
t.Fatalf("Sign error: %v", err)
|
||||
}
|
||||
rr := doAuth(svc.Authorize(RoleAdmin, RoleViewer)(claimsHandler(t)), tok)
|
||||
responseCode(t, rr, http.StatusOK)
|
||||
if rr.Header().Get("X-Claims-Sub") != "u" {
|
||||
t.Errorf("X-Claims-Sub = %q, want u", rr.Header().Get("X-Claims-Sub"))
|
||||
}
|
||||
if rr.Header().Get("X-Claims-Role") != "owner" {
|
||||
t.Errorf("X-Claims-Role = %q, want owner", rr.Header().Get("X-Claims-Role"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthorize_AdminSatisfiesViewer(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := New(testConfig(""))
|
||||
tok, err := svc.Sign("u", RoleAdmin, time.Hour)
|
||||
if err != nil {
|
||||
t.Fatalf("Sign error: %v", err)
|
||||
}
|
||||
rr := doAuth(svc.Authorize(RoleViewer)(claimsHandler(t)), tok)
|
||||
responseCode(t, rr, http.StatusOK)
|
||||
}
|
||||
|
||||
func TestAuthorize_AdminFailsOwner(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := New(testConfig(""))
|
||||
tok, err := svc.Sign("u", RoleAdmin, time.Hour)
|
||||
if err != nil {
|
||||
t.Fatalf("Sign error: %v", err)
|
||||
}
|
||||
rr := doAuth(svc.Authorize(RoleOwner)(claimsHandler(t)), tok)
|
||||
responseCode(t, rr, http.StatusForbidden)
|
||||
assertErrorCode(t, rr, "forbidden")
|
||||
}
|
||||
|
||||
func TestAuthorize_ViewerFailsAdmin(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := New(testConfig(""))
|
||||
tok, err := svc.Sign("u", RoleViewer, time.Hour)
|
||||
if err != nil {
|
||||
t.Fatalf("Sign error: %v", err)
|
||||
}
|
||||
rr := doAuth(svc.Authorize(RoleAdmin)(claimsHandler(t)), tok)
|
||||
responseCode(t, rr, http.StatusForbidden)
|
||||
assertErrorCode(t, rr, "forbidden")
|
||||
}
|
||||
|
||||
func TestAuthorize_AnyAuthenticated(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := New(testConfig(""))
|
||||
tok, err := svc.Sign("u", RoleViewer, time.Hour)
|
||||
if err != nil {
|
||||
t.Fatalf("Sign error: %v", err)
|
||||
}
|
||||
rr := doAuth(svc.Authorize()(claimsHandler(t)), tok)
|
||||
responseCode(t, rr, http.StatusOK)
|
||||
}
|
||||
|
||||
func TestAuthorize_BearerPrefixRequired(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := New(testConfig(""))
|
||||
tok, err := svc.Sign("u", RoleViewer, time.Hour)
|
||||
if err != nil {
|
||||
t.Fatalf("Sign error: %v", err)
|
||||
}
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
req.Header.Set("Authorization", tok) // no "Bearer " prefix
|
||||
rr := httptest.NewRecorder()
|
||||
svc.Authorize(RoleViewer)(claimsHandler(t)).ServeHTTP(rr, req)
|
||||
responseCode(t, rr, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
func TestClaimsFromContext_Missing(t *testing.T) {
|
||||
t.Parallel()
|
||||
if _, ok := ClaimsFromContext(context.Background()); ok {
|
||||
t.Fatal("ClaimsFromContext on empty ctx should return ok=false")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user