Skip to content

Commit 814dbeb

Browse files
committed
fix: update tenant annotations to be taken from root level.
1 parent b0141d1 commit 814dbeb

File tree

3 files changed

+37
-50
lines changed

3 files changed

+37
-50
lines changed

internal/indexer/consumer.go

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,12 @@ type Indexer struct {
2323
}
2424

2525
type auditEvent struct {
26-
AuditID string `json:"auditID"`
27-
Verb string `json:"verb"`
26+
AuditID string `json:"auditID"`
27+
Verb string `json:"verb"`
28+
Annotations map[string]string `json:"annotations"`
29+
User struct {
30+
Extra map[string][]string `json:"extra"`
31+
} `json:"user"`
2832
ObjectRef struct {
2933
APIGroup string `json:"apiGroup"`
3034
APIVersion string `json:"apiVersion"`
@@ -37,25 +41,22 @@ type auditEvent struct {
3741
}
3842

3943
// extractTenantFromAuditEvent extracts tenant identity from the audit event.
40-
// It reads exclusively from ResponseObject metadata annotations:
44+
// It reads exclusively from the top-level audit event annotations:
4145
// - ScopeTypeAnnotationKey ("platform.miloapis.com/scope.type") for the tenant type
4246
// - ScopeNameAnnotationKey ("platform.miloapis.com/scope.name") for the tenant name
4347
//
44-
// Falls back to "platform"/"platform" when the ResponseObject is absent or the
45-
// annotations are not set.
48+
// Falls back to "platform"/"platform" when the annotations are absent or not set.
4649
func extractTenantFromAuditEvent(event *auditEvent) (tenantName string, tenantType string) {
4750
tenantName = tenantTypePlatform
4851
tenantType = tenantTypePlatform
4952

50-
if event.ResponseObject == nil {
53+
if event.Annotations == nil {
5154
return
5255
}
5356

5457
caser := cases.Title(language.Und)
55-
obj := &unstructured.Unstructured{Object: event.ResponseObject}
56-
annotations := obj.GetAnnotations()
5758

58-
if v, ok := annotations[ScopeTypeAnnotationKey]; ok && v != "" {
59+
if v, ok := event.Annotations[ScopeTypeAnnotationKey]; ok && v != "" {
5960
// Normalize to title-case to match Milo's scope annotation conventions
6061
// (e.g. the annotation value "project" becomes "Project").
6162
// Exception: "platform" is a fallback default and stays lowercase.
@@ -66,7 +67,7 @@ func extractTenantFromAuditEvent(event *auditEvent) (tenantName string, tenantTy
6667
}
6768
}
6869

69-
if v, ok := annotations[ScopeNameAnnotationKey]; ok && v != "" {
70+
if v, ok := event.Annotations[ScopeNameAnnotationKey]; ok && v != "" {
7071
tenantName = v
7172
}
7273

internal/indexer/consumer_test.go

Lines changed: 9 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -172,15 +172,11 @@ func TestExtractTenantFromAuditEvent_NilResponseObject(t *testing.T) {
172172
}
173173

174174
func TestExtractTenantFromAuditEvent_WithAnnotations(t *testing.T) {
175-
// Both scope annotations present — should be extracted and type normalized to title-case.
175+
// Both scope annotations present at the top level — should be extracted and type normalized to title-case.
176176
event := &auditEvent{
177-
ResponseObject: map[string]any{
178-
"metadata": map[string]any{
179-
"annotations": map[string]any{
180-
ScopeTypeAnnotationKey: "project",
181-
ScopeNameAnnotationKey: "my-project",
182-
},
183-
},
177+
Annotations: map[string]string{
178+
ScopeTypeAnnotationKey: "project",
179+
ScopeNameAnnotationKey: "my-project",
184180
},
185181
}
186182

@@ -195,15 +191,11 @@ func TestExtractTenantFromAuditEvent_WithAnnotations(t *testing.T) {
195191
}
196192

197193
func TestExtractTenantFromAuditEvent_PartialAnnotations_TypeOnly(t *testing.T) {
198-
// Only scope.type annotation is set; scope.name is absent.
194+
// Only scope.type annotation is set at the top level; scope.name is absent.
199195
// Expect: tenantType is extracted, tenantName falls back to "platform".
200196
event := &auditEvent{
201-
ResponseObject: map[string]any{
202-
"metadata": map[string]any{
203-
"annotations": map[string]any{
204-
ScopeTypeAnnotationKey: "Project",
205-
},
206-
},
197+
Annotations: map[string]string{
198+
ScopeTypeAnnotationKey: "Project",
207199
},
208200
}
209201

@@ -218,14 +210,8 @@ func TestExtractTenantFromAuditEvent_PartialAnnotations_TypeOnly(t *testing.T) {
218210
}
219211

220212
func TestExtractTenantFromAuditEvent_NoAnnotations(t *testing.T) {
221-
// ResponseObject present but no scope annotations — should fall back to platform/platform.
222-
event := &auditEvent{
223-
ResponseObject: map[string]any{
224-
"metadata": map[string]any{
225-
"name": "some-resource",
226-
},
227-
},
228-
}
213+
// No top-level annotations — should fall back to platform/platform.
214+
event := &auditEvent{}
229215

230216
name, typ := extractTenantFromAuditEvent(event)
231217

test/e2e/search-flow-multi-tenant-audit/chainsaw-test.yaml

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,24 @@ metadata:
55
spec:
66
description: |
77
Validates that resources injected via NATS AUDIT_EVENTS with tenant metadata carried
8-
in user.extra fields are correctly indexed with _tenant/_tenant_type fields and that
9-
search results expose tenant.name and tenant.type. Since project control planes are
10-
not available in the test environment, this test directly publishes crafted audit
11-
events to NATS to simulate the multi-tenant audit event indexing path.
8+
in top-level audit event annotations are correctly indexed with _tenant/_tenant_type
9+
fields and that search results expose tenant.name and tenant.type. Since project
10+
control planes are not available in the test environment, this test directly publishes
11+
crafted audit events to NATS to simulate the multi-tenant audit event indexing path.
1212
1313
This exercises the Indexer.Start() path (not ReindexConsumer), which:
1414
1. Consumes from the AUDIT_EVENTS stream on subject audit.k8s.activity.
1515
2. Iterates all active ResourceIndexPolicies and evaluates responseObject against
1616
each policy's CEL conditions.
17-
3. Calls extractTenantFromAuditEvent() to read tenant identity from
18-
user.extra["iam.miloapis.com/parent-type"] and
19-
user.extra["iam.miloapis.com/parent-name"].
20-
4. Falls back to "platform"/"platform" when user.extra is absent.
17+
3. Calls extractTenantFromAuditEvent() to read tenant identity from the top-level
18+
annotations["platform.miloapis.com/scope.type"] and
19+
annotations["platform.miloapis.com/scope.name"] fields of the audit event.
20+
4. Falls back to "platform"/"platform" when top-level annotations are absent.
2121
2222
Covers:
2323
- A1: Project-tenant resource is searchable with tenant.name and tenant.type set.
24-
- A2: Platform-tenant resource (no user.extra) is searchable with fallback
25-
tenant.name="platform" and tenant.type="platform".
24+
- A2: Platform-tenant resource (no top-level annotations) is searchable with
25+
fallback tenant.name="platform" and tenant.type="platform".
2626
- A3: Mixed search returns both results with correct per-result tenant metadata.
2727
- _tenant/_tenant_type internal fields are not exposed on the resource object.
2828
@@ -97,16 +97,16 @@ spec:
9797
timeout: 120s
9898
content: |
9999
# Event A: project-tenant resource.
100-
# user.extra carries iam.miloapis.com/parent-type and parent-name so the
101-
# indexer's extractTenantFromAuditEvent() will resolve the tenant as
102-
# type="Project", name="e2e-audit-test-project".
103-
EVENT_A='{"auditID":"e2e-audit-project-event-001","verb":"create","objectRef":{"apiGroup":"rbac.authorization.k8s.io","apiVersion":"v1","resource":"rolebindings","name":"e2e-audit-project-role","namespace":"default","uid":"e2e-audit-project-uid-00000001"},"responseObject":{"apiVersion":"rbac.authorization.k8s.io/v1","kind":"RoleBinding","metadata":{"name":"e2e-audit-project-role","namespace":"default","uid":"e2e-audit-project-uid-00000001","labels":{"e2e-multi-tenant-audit":"true"},"annotations":{"e2e.search.test/service":"audit-project-service-mt-rn2","platform.miloapis.com/scope.type":"Project","platform.miloapis.com/scope.name":"e2e-audit-test-project"}},"roleRef":{"apiGroup":"rbac.authorization.k8s.io","kind":"Role","name":"view"},"subjects":[]},"user":{"extra":{"iam.miloapis.com/parent-type":["project"],"iam.miloapis.com/parent-name":["e2e-audit-test-project"]}}}'
100+
# Top-level annotations field carries platform.miloapis.com/scope.type and
101+
# scope.name so the indexer's extractTenantFromAuditEvent() will resolve
102+
# the tenant as type="Project", name="e2e-audit-test-project".
103+
EVENT_A='{"auditID":"e2e-audit-project-event-001","verb":"create","annotations":{"platform.miloapis.com/scope.type":"Project","platform.miloapis.com/scope.name":"e2e-audit-test-project"},"objectRef":{"apiGroup":"rbac.authorization.k8s.io","apiVersion":"v1","resource":"rolebindings","name":"e2e-audit-project-role","namespace":"default","uid":"e2e-audit-project-uid-00000001"},"responseObject":{"apiVersion":"rbac.authorization.k8s.io/v1","kind":"RoleBinding","metadata":{"name":"e2e-audit-project-role","namespace":"default","uid":"e2e-audit-project-uid-00000001","labels":{"e2e-multi-tenant-audit":"true"},"annotations":{"e2e.search.test/service":"audit-project-service-mt-rn2"}},"roleRef":{"apiGroup":"rbac.authorization.k8s.io","kind":"Role","name":"view"},"subjects":[]},"user":{"extra":{"iam.miloapis.com/parent-type":["project"],"iam.miloapis.com/parent-name":["e2e-audit-test-project"]}}}'
104104
105105
# Event B: platform-tenant resource.
106-
# No "user" field at all — exercises the fallback path in
106+
# No top-level annotations field — exercises the fallback path in
107107
# extractTenantFromAuditEvent() that returns "platform"/"platform"
108-
# when user.extra is nil.
109-
EVENT_B='{"auditID":"e2e-audit-platform-event-001","verb":"create","objectRef":{"apiGroup":"rbac.authorization.k8s.io","apiVersion":"v1","resource":"rolebindings","name":"e2e-audit-platform-role","namespace":"default","uid":"e2e-audit-platform-uid-00000001"},"responseObject":{"apiVersion":"rbac.authorization.k8s.io/v1","kind":"RoleBinding","metadata":{"name":"e2e-audit-platform-role","namespace":"default","uid":"e2e-audit-platform-uid-00000001","labels":{"e2e-multi-tenant-audit":"true"},"annotations":{"e2e.search.test/service":"audit-platform-service-mt-rn2","platform.miloapis.com/scope.type":"platform","platform.miloapis.com/scope.name":"platform"}},"roleRef":{"apiGroup":"rbac.authorization.k8s.io","kind":"Role","name":"view"},"subjects":[]}}'
108+
# when top-level annotations are absent.
109+
EVENT_B='{"auditID":"e2e-audit-platform-event-001","verb":"create","objectRef":{"apiGroup":"rbac.authorization.k8s.io","apiVersion":"v1","resource":"rolebindings","name":"e2e-audit-platform-role","namespace":"default","uid":"e2e-audit-platform-uid-00000001"},"responseObject":{"apiVersion":"rbac.authorization.k8s.io/v1","kind":"RoleBinding","metadata":{"name":"e2e-audit-platform-role","namespace":"default","uid":"e2e-audit-platform-uid-00000001","labels":{"e2e-multi-tenant-audit":"true"},"annotations":{"e2e.search.test/service":"audit-platform-service-mt-rn2"}},"roleRef":{"apiGroup":"rbac.authorization.k8s.io","kind":"Role","name":"view"},"subjects":[]}}'
110110
111111
# Store event payloads in a ConfigMap so they can be mounted into the
112112
# Job container without shell escaping issues.

0 commit comments

Comments
 (0)