Skip to content

Commit 7edc772

Browse files
committed
chore: get tenant annotation from annotations
1 parent 765e1a7 commit 7edc772

File tree

4 files changed

+72
-56
lines changed

4 files changed

+72
-56
lines changed

internal/indexer/consumer.go

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -34,30 +34,40 @@ type auditEvent struct {
3434
UID string `json:"uid"`
3535
} `json:"objectRef"`
3636
ResponseObject map[string]any `json:"responseObject"`
37-
// User carries authenticated user information including tenant context in extra fields.
38-
User *struct {
39-
Extra map[string][]string `json:"extra,omitempty"`
40-
} `json:"user,omitempty"`
4137
}
4238

43-
// extractTenantFromAuditEvent extracts tenant identity from audit event user extra fields.
44-
// Mirrors processor.ExtractTenant() in the Activity repo.
45-
// Falls back to "platform"/"platform" when fields are absent.
39+
// extractTenantFromAuditEvent extracts tenant identity from the audit event.
40+
// It reads exclusively from ResponseObject metadata annotations:
41+
// - ScopeTypeAnnotationKey ("platform.miloapis.com/scope.type") for the tenant type
42+
// - ScopeNameAnnotationKey ("platform.miloapis.com/scope.name") for the tenant name
43+
//
44+
// Falls back to "platform"/"platform" when the ResponseObject is absent or the
45+
// annotations are not set.
4646
func extractTenantFromAuditEvent(event *auditEvent) (tenantName string, tenantType string) {
47-
tenantName = "platform"
48-
tenantType = "platform"
47+
tenantName = tenantTypePlatform
48+
tenantType = tenantTypePlatform
4949

50-
if event.User == nil || event.User.Extra == nil {
50+
if event.ResponseObject == nil {
5151
return
5252
}
5353

54-
if values, ok := event.User.Extra["iam.miloapis.com/parent-type"]; ok && len(values) > 0 {
54+
caser := cases.Title(language.Und)
55+
obj := &unstructured.Unstructured{Object: event.ResponseObject}
56+
annotations := obj.GetAnnotations()
57+
58+
if v, ok := annotations[ScopeTypeAnnotationKey]; ok && v != "" {
5559
// Normalize to title-case to match Milo's scope annotation conventions
5660
// (e.g. the annotation value "project" becomes "Project").
57-
tenantType = cases.Title(language.Und).String(values[0])
61+
// Exception: "platform" is a fallback default and stays lowercase.
62+
if v != tenantTypePlatform {
63+
tenantType = caser.String(v)
64+
} else {
65+
tenantType = v
66+
}
5867
}
59-
if values, ok := event.User.Extra["iam.miloapis.com/parent-name"]; ok && len(values) > 0 {
60-
tenantName = values[0]
68+
69+
if v, ok := annotations[ScopeNameAnnotationKey]; ok && v != "" {
70+
tenantName = v
6171
}
6272

6373
return
@@ -81,6 +91,11 @@ const (
8191
// to avoid an import cycle: internal/tenant/project_watcher.go already
8292
// imports internal/indexer, so internal/indexer cannot import internal/tenant.
8393
tenantTypePlatform = "platform"
94+
95+
// Scope annotation keys from resource metadata. Tenant identity is derived
96+
// exclusively from these annotations on the ResponseObject.
97+
ScopeTypeAnnotationKey = "platform.miloapis.com/scope.type"
98+
ScopeNameAnnotationKey = "platform.miloapis.com/scope.name"
8499
)
85100

86101
// Start starts the indexer consumer loop.

internal/indexer/consumer_test.go

Lines changed: 39 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -157,50 +157,53 @@ func TestIndexer_Start_ConsumeFlow(t *testing.T) {
157157
msg.AssertExpectations(t)
158158
}
159159

160-
func TestExtractTenantFromAuditEvent_WithUserExtra(t *testing.T) {
160+
func TestExtractTenantFromAuditEvent_NilResponseObject(t *testing.T) {
161+
// No ResponseObject — should fall back to platform/platform.
161162
event := &auditEvent{}
162-
event.User = &struct {
163-
Extra map[string][]string `json:"extra,omitempty"`
164-
}{
165-
Extra: map[string][]string{
166-
"iam.miloapis.com/parent-type": {"Project"},
167-
"iam.miloapis.com/parent-name": {"my-project"},
168-
},
169-
}
170163

171164
name, typ := extractTenantFromAuditEvent(event)
172165

173-
if name != "my-project" {
174-
t.Errorf("tenantName: got %q, want %q", name, "my-project")
166+
if name != "platform" {
167+
t.Errorf("tenantName: got %q, want %q", name, "platform")
175168
}
176-
if typ != "Project" {
177-
t.Errorf("tenantType: got %q, want %q", typ, "Project")
169+
if typ != "platform" {
170+
t.Errorf("tenantType: got %q, want %q", typ, "platform")
178171
}
179172
}
180173

181-
func TestExtractTenantFromAuditEvent_NoUserExtra(t *testing.T) {
182-
event := &auditEvent{}
183-
// User.Extra is nil by default.
174+
func TestExtractTenantFromAuditEvent_WithAnnotations(t *testing.T) {
175+
// Both scope annotations present — should be extracted and type normalized to title-case.
176+
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+
},
184+
},
185+
}
184186

185187
name, typ := extractTenantFromAuditEvent(event)
186188

187-
if name != "platform" {
188-
t.Errorf("tenantName: got %q, want %q", name, "platform")
189+
if name != "my-project" {
190+
t.Errorf("tenantName: got %q, want %q", name, "my-project")
189191
}
190-
if typ != "platform" {
191-
t.Errorf("tenantType: got %q, want %q", typ, "platform")
192+
if typ != "Project" {
193+
t.Errorf("tenantType: got %q, want %q", typ, "Project")
192194
}
193195
}
194196

195-
func TestExtractTenantFromAuditEvent_PartialUserExtra_TypeOnlyNoName(t *testing.T) {
196-
// Only parent-type is set; parent-name is absent.
197-
// Expect: tenantType reflects the extra field, tenantName falls back to "platform".
198-
event := &auditEvent{}
199-
event.User = &struct {
200-
Extra map[string][]string `json:"extra,omitempty"`
201-
}{
202-
Extra: map[string][]string{
203-
"iam.miloapis.com/parent-type": {"Project"},
197+
func TestExtractTenantFromAuditEvent_PartialAnnotations_TypeOnly(t *testing.T) {
198+
// Only scope.type annotation is set; scope.name is absent.
199+
// Expect: tenantType is extracted, tenantName falls back to "platform".
200+
event := &auditEvent{
201+
ResponseObject: map[string]any{
202+
"metadata": map[string]any{
203+
"annotations": map[string]any{
204+
ScopeTypeAnnotationKey: "Project",
205+
},
206+
},
204207
},
205208
}
206209

@@ -214,15 +217,13 @@ func TestExtractTenantFromAuditEvent_PartialUserExtra_TypeOnlyNoName(t *testing.
214217
}
215218
}
216219

217-
func TestExtractTenantFromAuditEvent_EmptySliceValues(t *testing.T) {
218-
// Keys present but with empty slices should not override the defaults.
219-
event := &auditEvent{}
220-
event.User = &struct {
221-
Extra map[string][]string `json:"extra,omitempty"`
222-
}{
223-
Extra: map[string][]string{
224-
"iam.miloapis.com/parent-type": {},
225-
"iam.miloapis.com/parent-name": {},
220+
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+
},
226227
},
227228
}
228229

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,13 +100,13 @@ spec:
100100
# user.extra carries iam.miloapis.com/parent-type and parent-name so the
101101
# indexer's extractTenantFromAuditEvent() will resolve the tenant as
102102
# 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"}},"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"]}}}'
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"]}}}'
104104
105105
# Event B: platform-tenant resource.
106106
# No "user" field at all — exercises the fallback path in
107107
# extractTenantFromAuditEvent() that returns "platform"/"platform"
108108
# 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"}},"roleRef":{"apiGroup":"rbac.authorization.k8s.io","kind":"Role","name":"view"},"subjects":[]}}'
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":[]}}'
110110
111111
# Store event payloads in a ConfigMap so they can be mounted into the
112112
# Job container without shell escaping issues.

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,11 +94,11 @@ spec:
9494
# Build event JSON with INDEX_NAME substituted.
9595
# SpecHash is intentionally omitted so the consumer skips the hash check.
9696
EVENT1=$(printf \
97-
'{"id":"e2e-mt-project-resource-001","policyName":"e2e-multi-tenant-role-policy","indexName":"%s","tenant":"e2e-test-project","tenantType":"Project","resource":{"apiVersion":"rbac.authorization.k8s.io/v1","kind":"ClusterRole","metadata":{"name":"e2e-project-role","uid":"e2e-project-uid-00000001","labels":{"e2e-multi-tenant":"true"},"annotations":{"e2e.search.test/service":"project-service-mt-rn1"}},"rules":[{"apiGroups":[""],"resources":["pods"],"verbs":["get"]}]}}' \
97+
'{"id":"e2e-mt-project-resource-001","policyName":"e2e-multi-tenant-role-policy","indexName":"%s","tenant":"e2e-test-project","tenantType":"Project","resource":{"apiVersion":"rbac.authorization.k8s.io/v1","kind":"ClusterRole","metadata":{"name":"e2e-project-role","uid":"e2e-project-uid-00000001","labels":{"e2e-multi-tenant":"true"},"annotations":{"e2e.search.test/service":"project-service-mt-rn1","platform.miloapis.com/scope.type":"Project","platform.miloapis.com/scope.name":"e2e-test-project"}},"rules":[{"apiGroups":[""],"resources":["pods"],"verbs":["get"]}]}}' \
9898
"$INDEX_NAME")
9999
100100
EVENT2=$(printf \
101-
'{"id":"e2e-mt-platform-resource-001","policyName":"e2e-multi-tenant-role-policy","indexName":"%s","tenant":"platform","tenantType":"platform","resource":{"apiVersion":"rbac.authorization.k8s.io/v1","kind":"ClusterRole","metadata":{"name":"e2e-platform-role","uid":"e2e-platform-uid-00000001","labels":{"e2e-multi-tenant":"true"},"annotations":{"e2e.search.test/service":"platform-service-mt-rn1"}},"rules":[{"apiGroups":[""],"resources":["pods"],"verbs":["get"]}]}}' \
101+
'{"id":"e2e-mt-platform-resource-001","policyName":"e2e-multi-tenant-role-policy","indexName":"%s","tenant":"platform","tenantType":"platform","resource":{"apiVersion":"rbac.authorization.k8s.io/v1","kind":"ClusterRole","metadata":{"name":"e2e-platform-role","uid":"e2e-platform-uid-00000001","labels":{"e2e-multi-tenant":"true"},"annotations":{"e2e.search.test/service":"platform-service-mt-rn1","platform.miloapis.com/scope.type":"platform","platform.miloapis.com/scope.name":"platform"}},"rules":[{"apiGroups":[""],"resources":["pods"],"verbs":["get"]}]}}' \
102102
"$INDEX_NAME")
103103
104104
# Store event payloads in a ConfigMap so they can be mounted into the

0 commit comments

Comments
 (0)