Skip to content

Commit 60ace1a

Browse files
committed
Add audit to PDP and CE
1 parent 53df728 commit 60ace1a

12 files changed

Lines changed: 769 additions & 75 deletions

File tree

audit-service/config/config.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,18 @@ var (
3636
// DefaultEnums provides default enum values if config file is not found
3737
DefaultEnums = AuditEnums{
3838
EventTypes: []string{
39+
"ORCHESTRATION_REQUEST_RECEIVED",
3940
"POLICY_CHECK",
41+
"POLICY_CHECK_REQUEST",
42+
"POLICY_CHECK_RESPONSE",
43+
"CONSENT_CHECK",
44+
"CONSENT_CHECK_REQUEST",
45+
"CONSENT_CHECK_RESPONSE",
46+
"PROVIDER_FETCH_REQUEST",
47+
"PROVIDER_FETCH_RESPONSE",
4048
"MANAGEMENT_EVENT",
4149
"USER_MANAGEMENT",
4250
"DATA_FETCH",
43-
"CONSENT_CHECK",
4451
},
4552
EventActions: []string{
4653
"CREATE",

audit-service/config/enums.yaml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,18 @@ enums:
66
# Event Type: User-defined custom names for event classification
77
# Examples: POLICY_CHECK, MANAGEMENT_EVENT, USER_MANAGEMENT, etc.
88
eventTypes:
9+
- ORCHESTRATION_REQUEST_RECEIVED
910
- POLICY_CHECK
11+
- POLICY_CHECK_REQUEST
12+
- POLICY_CHECK_RESPONSE
13+
- CONSENT_CHECK
14+
- CONSENT_CHECK_REQUEST
15+
- CONSENT_CHECK_RESPONSE
16+
- PROVIDER_FETCH_REQUEST
17+
- PROVIDER_FETCH_RESPONSE
1018
- MANAGEMENT_EVENT
1119
- USER_MANAGEMENT
1220
- DATA_FETCH
13-
- CONSENT_CHECK
1421

1522
# Event Action: CRUD operations
1623
eventActions:

exchange/consent-engine/go.mod

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ go 1.24.6
55
require (
66
github.com/golang-jwt/jwt/v5 v5.3.0
77
github.com/google/uuid v1.6.0
8+
github.com/gov-dx-sandbox/audit-service v0.0.0-00010101000000-000000000000
9+
github.com/gov-dx-sandbox/exchange/shared/audit v0.0.0-00010101000000-000000000000
810
github.com/gov-dx-sandbox/exchange/shared/config v0.0.0
911
github.com/gov-dx-sandbox/exchange/shared/monitoring v0.0.0-00010101000000-000000000000
1012
github.com/gov-dx-sandbox/exchange/shared/utils v0.0.0
@@ -38,7 +40,6 @@ require (
3840
github.com/prometheus/client_model v0.6.1 // indirect
3941
github.com/prometheus/common v0.60.1 // indirect
4042
github.com/prometheus/procfs v0.15.1 // indirect
41-
github.com/rogpeppe/go-internal v1.14.1 // indirect
4243
go.opentelemetry.io/otel v1.32.0 // indirect
4344
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0 // indirect
4445
go.opentelemetry.io/otel/exporters/prometheus v0.54.0 // indirect
@@ -59,6 +60,10 @@ require (
5960
gopkg.in/yaml.v3 v3.0.1 // indirect
6061
)
6162

63+
replace github.com/gov-dx-sandbox/audit-service => ../../audit-service
64+
65+
replace github.com/gov-dx-sandbox/exchange/shared/audit => ../shared/audit
66+
6267
replace github.com/gov-dx-sandbox/exchange/shared/config => ./shared/config
6368

6469
replace github.com/gov-dx-sandbox/exchange/shared/constants => ./shared/constants

exchange/consent-engine/main.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
v1auth "github.com/gov-dx-sandbox/exchange/consent-engine/v1/auth"
1515
v1db "github.com/gov-dx-sandbox/exchange/consent-engine/v1/database"
1616
v1handlers "github.com/gov-dx-sandbox/exchange/consent-engine/v1/handlers"
17+
v1middleware "github.com/gov-dx-sandbox/exchange/consent-engine/v1/middleware"
1718
v1router "github.com/gov-dx-sandbox/exchange/consent-engine/v1/router"
1819
v1services "github.com/gov-dx-sandbox/exchange/consent-engine/v1/services"
1920
)
@@ -82,6 +83,15 @@ func main() {
8283
os.Exit(1)
8384
}
8485

86+
// Initialize audit middleware (optional - can be disabled via ENABLE_AUDIT=false or empty URL)
87+
auditServiceURL := getEnvOrDefault("CHOREO_AUDIT_CONNECTION_SERVICEURL", "")
88+
v1middleware.NewAuditMiddleware(auditServiceURL)
89+
if auditServiceURL != "" {
90+
slog.Info("Audit logging enabled", "auditServiceURL", auditServiceURL)
91+
} else {
92+
slog.Info("Audit logging disabled (no audit service URL configured)")
93+
}
94+
8595
// Initialize V1 handlers
8696
v1InternalHandler := v1handlers.NewInternalHandler(v1ConsentService)
8797
v1PortalHandler := v1handlers.NewPortalHandler(v1ConsentService)

exchange/consent-engine/v1/handlers/internal_handler.go

Lines changed: 115 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
package handlers
22

33
import (
4+
"context"
45
"encoding/json"
56
"errors"
67
"fmt"
78
"log/slog"
89
"net/http"
10+
"time"
911

12+
auditmodels "github.com/gov-dx-sandbox/audit-service/v1/models"
13+
"github.com/gov-dx-sandbox/exchange/consent-engine/v1/middleware"
1014
"github.com/gov-dx-sandbox/exchange/consent-engine/v1/models"
1115
"github.com/gov-dx-sandbox/exchange/consent-engine/v1/services"
1216
"github.com/gov-dx-sandbox/exchange/consent-engine/v1/utils"
@@ -46,6 +50,9 @@ func (h *InternalHandler) GetConsent(w http.ResponseWriter, r *http.Request) {
4650
return
4751
}
4852

53+
// Extract trace ID from request and add to context
54+
ctx := middleware.ExtractTraceIDFromRequest(r)
55+
4956
// Parse query parameters
5057
ownerEmail := r.URL.Query().Get("ownerEmail")
5158
ownerID := r.URL.Query().Get("ownerId")
@@ -62,20 +69,26 @@ func (h *InternalHandler) GetConsent(w http.ResponseWriter, r *http.Request) {
6269
return
6370
}
6471

72+
// Log consent check request
73+
h.logConsentCheckRequest(ctx, appID, ownerEmail, ownerID)
74+
6575
// Get consent from service (context with timeout is propagated)
6676
var consent *models.ConsentResponseInternalView
6777
var err error
6878

6979
if ownerEmail != "" {
70-
consent, err = h.consentService.GetConsentInternalView(r.Context(), nil, nil, &ownerEmail, &appID)
80+
consent, err = h.consentService.GetConsentInternalView(ctx, nil, nil, &ownerEmail, &appID)
7181
} else {
72-
consent, err = h.consentService.GetConsentInternalView(r.Context(), nil, &ownerID, nil, &appID)
82+
consent, err = h.consentService.GetConsentInternalView(ctx, nil, &ownerID, nil, &appID)
7383
}
7484

85+
// Log consent check response
86+
h.logConsentCheckResponse(ctx, appID, consent, err)
87+
7588
if err != nil {
7689
// Check if error is due to context cancellation or timeout
77-
if r.Context().Err() != nil {
78-
slog.Warn("Request context cancelled during service call", "error", r.Context().Err())
90+
if ctx.Err() != nil {
91+
slog.Warn("Request context cancelled during service call", "error", ctx.Err())
7992
utils.RespondWithError(w, http.StatusRequestTimeout, models.ErrorCodeInternalError, "Request timeout or cancelled")
8093
return
8194
}
@@ -91,6 +104,104 @@ func (h *InternalHandler) GetConsent(w http.ResponseWriter, r *http.Request) {
91104
utils.RespondWithJSON(w, http.StatusOK, consent)
92105
}
93106

107+
// logConsentCheckRequest logs a CONSENT_CHECK_REQUEST event from consent-engine's perspective
108+
func (h *InternalHandler) logConsentCheckRequest(ctx context.Context, appID, ownerEmail, ownerID string) {
109+
traceID := middleware.GetTraceIDFromContext(ctx)
110+
if traceID == "" {
111+
return
112+
}
113+
114+
eventType := "CONSENT_CHECK_REQUEST"
115+
actorType := "SERVICE"
116+
actorID := "consent-engine"
117+
targetType := "SERVICE"
118+
targetID := "orchestration-engine"
119+
120+
requestMetadata := map[string]interface{}{
121+
"applicationId": appID,
122+
}
123+
if ownerEmail != "" {
124+
requestMetadata["ownerEmail"] = ownerEmail
125+
}
126+
if ownerID != "" {
127+
requestMetadata["ownerId"] = ownerID
128+
}
129+
130+
requestMetadataJSON, err := json.Marshal(requestMetadata)
131+
if err != nil {
132+
slog.Error("Failed to marshal request metadata for audit", "error", err)
133+
return
134+
}
135+
136+
auditRequest := &auditmodels.CreateAuditLogRequest{
137+
TraceID: &traceID,
138+
Timestamp: time.Now().UTC().Format(time.RFC3339),
139+
EventType: &eventType,
140+
Status: auditmodels.StatusSuccess,
141+
ActorType: actorType,
142+
ActorID: actorID,
143+
TargetType: targetType,
144+
TargetID: &targetID,
145+
RequestMetadata: json.RawMessage(requestMetadataJSON),
146+
}
147+
148+
middleware.LogGeneralizedAuditEvent(ctx, auditRequest)
149+
}
150+
151+
// logConsentCheckResponse logs a CONSENT_CHECK_RESPONSE event from consent-engine's perspective
152+
func (h *InternalHandler) logConsentCheckResponse(ctx context.Context, appID string, consent *models.ConsentResponseInternalView, err error) {
153+
traceID := middleware.GetTraceIDFromContext(ctx)
154+
if traceID == "" {
155+
return
156+
}
157+
158+
eventType := "CONSENT_CHECK_RESPONSE"
159+
actorType := "SERVICE"
160+
actorID := "consent-engine"
161+
targetType := "SERVICE"
162+
targetID := "orchestration-engine"
163+
164+
status := auditmodels.StatusSuccess
165+
responseMetadata := make(map[string]interface{})
166+
167+
if err != nil {
168+
status = auditmodels.StatusFailure
169+
responseMetadata["error"] = err.Error()
170+
if errors.Is(err, models.ErrConsentNotFound) {
171+
responseMetadata["consentNotFound"] = true
172+
}
173+
} else if consent != nil {
174+
responseMetadata["consentId"] = consent.ConsentID
175+
responseMetadata["status"] = consent.Status
176+
if consent.ConsentPortalURL != nil {
177+
responseMetadata["consentPortalUrl"] = *consent.ConsentPortalURL
178+
}
179+
if consent.Fields != nil {
180+
responseMetadata["fieldsCount"] = len(*consent.Fields)
181+
}
182+
}
183+
184+
responseMetadataJSON, jsonErr := json.Marshal(responseMetadata)
185+
if jsonErr != nil {
186+
slog.Error("Failed to marshal response metadata for audit", "error", jsonErr)
187+
responseMetadataJSON = []byte("{}")
188+
}
189+
190+
auditRequest := &auditmodels.CreateAuditLogRequest{
191+
TraceID: &traceID,
192+
Timestamp: time.Now().UTC().Format(time.RFC3339),
193+
EventType: &eventType,
194+
Status: status,
195+
ActorType: actorType,
196+
ActorID: actorID,
197+
TargetType: targetType,
198+
TargetID: &targetID,
199+
ResponseMetadata: json.RawMessage(responseMetadataJSON),
200+
}
201+
202+
middleware.LogGeneralizedAuditEvent(ctx, auditRequest)
203+
}
204+
94205
// CreateConsent handles POST /internal/api/v1/consents
95206
// Body: models.CreateConsentRequest
96207
// Returns: []models.ConsentResponseInternalView
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package middleware
2+
3+
import (
4+
"context"
5+
"log/slog"
6+
"net/http"
7+
"sync"
8+
9+
"github.com/google/uuid"
10+
"github.com/gov-dx-sandbox/audit-service/v1/models"
11+
auditclient "github.com/gov-dx-sandbox/exchange/shared/audit"
12+
)
13+
14+
// TraceIDHeader is the HTTP header name for trace ID
15+
const TraceIDHeader = "X-Trace-ID"
16+
17+
// traceIDKey is the context key for trace ID
18+
type traceIDKey struct{}
19+
20+
// AuditMiddleware handles audit logging
21+
type AuditMiddleware struct {
22+
client *auditclient.Client
23+
}
24+
25+
// Global audit middleware instance for easy access from handlers
26+
var (
27+
globalAuditMiddleware *AuditMiddleware
28+
globalAuditOnce sync.Once
29+
)
30+
31+
// NewAuditMiddleware creates a new audit middleware with thread-safe global initialization
32+
// Audit can be disabled by:
33+
// - Setting ENABLE_AUDIT=false environment variable
34+
// - Providing an empty auditServiceURL
35+
//
36+
// When disabled, the middleware will skip all audit logging operations but services
37+
// will continue to function normally.
38+
func NewAuditMiddleware(auditServiceURL string) *AuditMiddleware {
39+
client := auditclient.NewClient(auditServiceURL)
40+
middleware := &AuditMiddleware{client: client}
41+
42+
globalAuditOnce.Do(func() {
43+
globalAuditMiddleware = middleware
44+
})
45+
46+
return middleware
47+
}
48+
49+
// LogGeneralizedAuditEvent logs a generalized audit event using global audit middleware instance
50+
func LogGeneralizedAuditEvent(ctx context.Context, auditRequest *models.CreateAuditLogRequest) {
51+
if globalAuditMiddleware != nil {
52+
globalAuditMiddleware.client.LogEvent(ctx, auditRequest)
53+
} else {
54+
slog.Warn("Global AuditMiddleware is not initialized; audit event not logged")
55+
}
56+
}
57+
58+
// GetTraceIDFromContext retrieves the trace ID from the context
59+
// Returns empty string if trace ID is not found in context
60+
func GetTraceIDFromContext(ctx context.Context) string {
61+
if traceID, ok := ctx.Value(traceIDKey{}).(string); ok {
62+
return traceID
63+
}
64+
return ""
65+
}
66+
67+
// ExtractTraceIDFromRequest extracts trace ID from HTTP header and adds it to context
68+
// If no trace ID is found in header, generates a new one
69+
func ExtractTraceIDFromRequest(r *http.Request) context.Context {
70+
traceID := r.Header.Get(TraceIDHeader)
71+
if traceID == "" {
72+
traceID = uuid.New().String()
73+
}
74+
return context.WithValue(r.Context(), traceIDKey{}, traceID)
75+
}

0 commit comments

Comments
 (0)