feat: sync lijiaoqiao implementation and staging validation artifacts
This commit is contained in:
295
platform-token-runtime/internal/token/audit_executable_test.go
Normal file
295
platform-token-runtime/internal/token/audit_executable_test.go
Normal file
@@ -0,0 +1,295 @@
|
||||
package token_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"lijiaoqiao/platform-token-runtime/internal/auth/middleware"
|
||||
"lijiaoqiao/platform-token-runtime/internal/auth/model"
|
||||
"lijiaoqiao/platform-token-runtime/internal/auth/service"
|
||||
)
|
||||
|
||||
func TestTOKAud001IssueSuccessEvent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
auditor := service.NewMemoryAuditEmitter()
|
||||
rt := service.NewInMemoryTokenRuntime(nil)
|
||||
|
||||
record, err := rt.IssueAndAudit(context.Background(), service.IssueTokenInput{
|
||||
SubjectID: "2001",
|
||||
Role: model.RoleOwner,
|
||||
Scope: []string{"supply:*"},
|
||||
TTL: 10 * time.Minute,
|
||||
RequestID: "req-aud-001",
|
||||
}, auditor)
|
||||
if err != nil {
|
||||
t.Fatalf("issue with audit failed: %v", err)
|
||||
}
|
||||
|
||||
event, ok := auditor.LastEvent()
|
||||
if !ok {
|
||||
t.Fatalf("expected issue success event")
|
||||
}
|
||||
if event.EventName != service.EventTokenIssueSuccess {
|
||||
t.Fatalf("unexpected event name: got=%s want=%s", event.EventName, service.EventTokenIssueSuccess)
|
||||
}
|
||||
assertAuditRequiredFields(t, event)
|
||||
if event.TokenID != record.TokenID {
|
||||
t.Fatalf("unexpected token_id in event: got=%s want=%s", event.TokenID, record.TokenID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTOKAud002IssueFailEvent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
auditor := service.NewMemoryAuditEmitter()
|
||||
rt := service.NewInMemoryTokenRuntime(nil)
|
||||
|
||||
_, err := rt.IssueAndAudit(context.Background(), service.IssueTokenInput{
|
||||
SubjectID: "2001",
|
||||
Role: model.RoleOwner,
|
||||
Scope: []string{"supply:*"},
|
||||
TTL: 0,
|
||||
RequestID: "req-aud-002",
|
||||
}, auditor)
|
||||
if err == nil {
|
||||
t.Fatalf("expected issue failure")
|
||||
}
|
||||
|
||||
event, ok := auditor.LastEvent()
|
||||
if !ok {
|
||||
t.Fatalf("expected issue fail event")
|
||||
}
|
||||
if event.EventName != service.EventTokenIssueFail {
|
||||
t.Fatalf("unexpected event name: got=%s want=%s", event.EventName, service.EventTokenIssueFail)
|
||||
}
|
||||
assertAuditRequiredFields(t, event)
|
||||
if event.ResultCode != "ISSUE_FAILED" {
|
||||
t.Fatalf("unexpected result_code: got=%s want=ISSUE_FAILED", event.ResultCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTOKAud003AuthnFailEvent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
auditor := service.NewMemoryAuditEmitter()
|
||||
rt := service.NewInMemoryTokenRuntime(nil)
|
||||
authorizer := service.NewScopeRoleAuthorizer()
|
||||
|
||||
handler := middleware.BuildTokenAuthChain(middleware.AuthMiddlewareConfig{
|
||||
Verifier: rt,
|
||||
StatusResolver: rt,
|
||||
Authorizer: authorizer,
|
||||
Auditor: auditor,
|
||||
}, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/supply/accounts", nil)
|
||||
req.Header.Set("Authorization", "Bearer invalid-token")
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("unexpected status code: got=%d want=%d", rec.Code, http.StatusUnauthorized)
|
||||
}
|
||||
event, ok := auditor.LastEvent()
|
||||
if !ok {
|
||||
t.Fatalf("expected audit event for authn failure")
|
||||
}
|
||||
if event.EventName != service.EventTokenAuthnFail {
|
||||
t.Fatalf("unexpected event name: got=%s want=%s", event.EventName, service.EventTokenAuthnFail)
|
||||
}
|
||||
if event.RequestID == "" {
|
||||
t.Fatalf("request_id must not be empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTOKAud004AuthzDeniedEvent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
auditor := service.NewMemoryAuditEmitter()
|
||||
rt := service.NewInMemoryTokenRuntime(nil)
|
||||
authorizer := service.NewScopeRoleAuthorizer()
|
||||
|
||||
ctx := context.Background()
|
||||
viewer, err := rt.Issue(ctx, service.IssueTokenInput{
|
||||
SubjectID: "2002",
|
||||
Role: model.RoleViewer,
|
||||
Scope: []string{"supply:read"},
|
||||
TTL: 5 * time.Minute,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("issue viewer token failed: %v", err)
|
||||
}
|
||||
|
||||
handler := middleware.BuildTokenAuthChain(middleware.AuthMiddlewareConfig{
|
||||
Verifier: rt,
|
||||
StatusResolver: rt,
|
||||
Authorizer: authorizer,
|
||||
Auditor: auditor,
|
||||
}, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/supply/packages", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+viewer.AccessToken)
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusForbidden {
|
||||
t.Fatalf("unexpected status code: got=%d want=%d", rec.Code, http.StatusForbidden)
|
||||
}
|
||||
event, ok := auditor.LastEvent()
|
||||
if !ok {
|
||||
t.Fatalf("expected audit event for authz denial")
|
||||
}
|
||||
if event.EventName != service.EventTokenAuthzDenied {
|
||||
t.Fatalf("unexpected event name: got=%s want=%s", event.EventName, service.EventTokenAuthzDenied)
|
||||
}
|
||||
if event.SubjectID != viewer.SubjectID {
|
||||
t.Fatalf("unexpected subject_id: got=%s want=%s", event.SubjectID, viewer.SubjectID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTOKAud005RevokeSuccessEvent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
auditor := service.NewMemoryAuditEmitter()
|
||||
rt := service.NewInMemoryTokenRuntime(nil)
|
||||
|
||||
record, err := rt.Issue(context.Background(), service.IssueTokenInput{
|
||||
SubjectID: "2001",
|
||||
Role: model.RoleOwner,
|
||||
Scope: []string{"supply:*"},
|
||||
TTL: 8 * time.Minute,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("issue token failed: %v", err)
|
||||
}
|
||||
_, err = rt.RevokeAndAudit(context.Background(), record.TokenID, "operator_request", "req-aud-005", record.SubjectID, auditor)
|
||||
if err != nil {
|
||||
t.Fatalf("revoke with audit failed: %v", err)
|
||||
}
|
||||
|
||||
event, ok := auditor.LastEvent()
|
||||
if !ok {
|
||||
t.Fatalf("expected revoke success event")
|
||||
}
|
||||
if event.EventName != service.EventTokenRevokeSuccess {
|
||||
t.Fatalf("unexpected event name: got=%s want=%s", event.EventName, service.EventTokenRevokeSuccess)
|
||||
}
|
||||
assertAuditRequiredFields(t, event)
|
||||
if event.TokenID != record.TokenID {
|
||||
t.Fatalf("unexpected token_id in event: got=%s want=%s", event.TokenID, record.TokenID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTOKAud006QueryKeyRejectedEvent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
auditor := service.NewMemoryAuditEmitter()
|
||||
rt := service.NewInMemoryTokenRuntime(nil)
|
||||
authorizer := service.NewScopeRoleAuthorizer()
|
||||
|
||||
handler := middleware.BuildTokenAuthChain(middleware.AuthMiddlewareConfig{
|
||||
Verifier: rt,
|
||||
StatusResolver: rt,
|
||||
Authorizer: authorizer,
|
||||
Auditor: auditor,
|
||||
}, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/supply/accounts?api_key=raw-secret-value", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("unexpected status code: got=%d want=%d", rec.Code, http.StatusUnauthorized)
|
||||
}
|
||||
event, ok := auditor.LastEvent()
|
||||
if !ok {
|
||||
t.Fatalf("expected query key rejection audit event")
|
||||
}
|
||||
if event.EventName != service.EventTokenQueryKeyRejected {
|
||||
t.Fatalf("unexpected event name: got=%s want=%s", event.EventName, service.EventTokenQueryKeyRejected)
|
||||
}
|
||||
|
||||
serialized := strings.Join([]string{
|
||||
event.EventID,
|
||||
event.EventName,
|
||||
event.RequestID,
|
||||
event.TokenID,
|
||||
event.SubjectID,
|
||||
event.Route,
|
||||
event.ResultCode,
|
||||
event.ClientIP,
|
||||
}, "|")
|
||||
if strings.Contains(serialized, "raw-secret-value") {
|
||||
t.Fatalf("audit event must not contain raw query key value")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTOKAud007EventImmutability(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
auditor := service.NewMemoryAuditEmitter()
|
||||
rt := service.NewInMemoryTokenRuntime(nil)
|
||||
|
||||
issued, err := rt.IssueAndAudit(context.Background(), service.IssueTokenInput{
|
||||
SubjectID: "2001",
|
||||
Role: model.RoleOwner,
|
||||
Scope: []string{"supply:*"},
|
||||
TTL: 20 * time.Minute,
|
||||
RequestID: "req-aud-007-1",
|
||||
}, auditor)
|
||||
if err != nil {
|
||||
t.Fatalf("issue with audit failed: %v", err)
|
||||
}
|
||||
_, err = rt.RevokeAndAudit(context.Background(), issued.TokenID, "test", "req-aud-007-2", issued.SubjectID, auditor)
|
||||
if err != nil {
|
||||
t.Fatalf("revoke with audit failed: %v", err)
|
||||
}
|
||||
|
||||
firstRead := auditor.Events()
|
||||
secondRead := auditor.Events()
|
||||
if len(firstRead) < 2 || len(secondRead) < 2 {
|
||||
t.Fatalf("expected at least two audit events")
|
||||
}
|
||||
for idx := range firstRead {
|
||||
if firstRead[idx].EventID != secondRead[idx].EventID ||
|
||||
firstRead[idx].EventName != secondRead[idx].EventName ||
|
||||
!firstRead[idx].CreatedAt.Equal(secondRead[idx].CreatedAt) {
|
||||
t.Fatalf("event should be immutable across reads at index=%d", idx)
|
||||
}
|
||||
}
|
||||
for idx := 1; idx < len(firstRead); idx++ {
|
||||
if firstRead[idx].CreatedAt.Before(firstRead[idx-1].CreatedAt) {
|
||||
t.Fatalf("event timeline should be ordered by created_at")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func assertAuditRequiredFields(t *testing.T, event service.AuditEvent) {
|
||||
t.Helper()
|
||||
if event.EventID == "" {
|
||||
t.Fatalf("event_id must not be empty")
|
||||
}
|
||||
if event.RequestID == "" {
|
||||
t.Fatalf("request_id must not be empty")
|
||||
}
|
||||
if event.ResultCode == "" {
|
||||
t.Fatalf("result_code must not be empty")
|
||||
}
|
||||
if event.Route == "" {
|
||||
t.Fatalf("route must not be empty")
|
||||
}
|
||||
if event.CreatedAt.IsZero() {
|
||||
t.Fatalf("created_at must not be zero")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user