Skip to content

Commit 99ba5ee

Browse files
committed
feat(casbackend): redact AP ARN and provider ID from managed-backend wire output
For Managed=true CAS backends, replace Location with "managed by Chainloop" and Provider with "Chainloop" everywhere the controlplane emits a CASBackend outside its trust boundary: * API responses (bizCASBackendToPb), so `chainloop cas-backend ls` no longer prints the AWS account ID, region, or AP name. * Audit-log events on the NATS bus (CASBackendCreated, CASBackendUpdated, CASBackendDeleted, CASBackendPermanentDeleted, CASBackendStatusChanged), so downstream consumers can't surface the same details to tenants either. The DB and biz layer continue to carry the real ARN and provider ID unchanged, so PerformValidation, the platform reconciler, and any forensic join by CASBackendID still work. Two helpers (displayLocation, displayProvider) keep the sanitization rule in one place. Assisted-by: Claude Code Signed-off-by: Jose I. Paris <jiparis@chainloop.dev> Chainloop-Trace-Sessions: 234a03ed-b238-4506-95f0-235242842db2
1 parent b0a7661 commit 99ba5ee

3 files changed

Lines changed: 139 additions & 12 deletions

File tree

app/controlplane/internal/service/casbackend.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,13 +193,25 @@ func (s *CASBackendService) Revalidate(ctx context.Context, req *pb.CASBackendSe
193193
}
194194

195195
func bizCASBackendToPb(in *biz.CASBackend) *pb.CASBackendItem {
196+
// Managed backends hide both Location (AP ARN) and Provider
197+
// (AWS-S3-ACCESS-POINT) from API clients — both are implementation
198+
// details that tenants don't need to know. The DB and biz layer
199+
// still carry the real values; only the wire format is sanitized.
200+
// See biz.CASBackendManagedLocationDisplay /
201+
// biz.CASBackendManagedProviderDisplay.
202+
location := in.Location
203+
provider := string(in.Provider)
204+
if in.Managed {
205+
location = biz.CASBackendManagedLocationDisplay
206+
provider = biz.CASBackendManagedProviderDisplay
207+
}
196208
r := &pb.CASBackendItem{
197-
Id: in.ID.String(), Location: in.Location, Description: in.Description,
209+
Id: in.ID.String(), Location: location, Description: in.Description,
198210
Name: in.Name,
199211
CreatedAt: timestamppb.New(*in.CreatedAt),
200212
UpdatedAt: timestamppb.New(*in.UpdatedAt),
201213
ValidatedAt: timestamppb.New(*in.ValidatedAt),
202-
Provider: string(in.Provider),
214+
Provider: provider,
203215
Default: in.Default,
204216
Fallback: in.Fallback,
205217
IsInline: in.Inline,
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
//
2+
// Copyright 2026 The Chainloop Authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package service
17+
18+
import (
19+
"testing"
20+
"time"
21+
22+
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz"
23+
"github.com/google/uuid"
24+
"github.com/stretchr/testify/assert"
25+
)
26+
27+
// TestBizCASBackendToPb_HidesManagedDetails guards the rule that
28+
// managed-backend implementation details (AP ARN, provider ID) never
29+
// leak to API clients. Non-managed rows keep their original Location
30+
// and Provider; managed rows are rewritten to stable placeholders.
31+
// Regression-prevention only — both fields are otherwise straightforward
32+
// to map.
33+
func TestBizCASBackendToPb_HidesManagedDetails(t *testing.T) {
34+
now := time.Now()
35+
realLocation := "arn:aws:s3:us-east-1:471112941097:accesspoint/chainloop-org-dev"
36+
realProvider := biz.CASBackendProvider("AWS-S3-ACCESS-POINT")
37+
38+
base := biz.CASBackend{
39+
ID: uuid.New(),
40+
Name: "backend",
41+
Location: realLocation,
42+
CreatedAt: &now,
43+
UpdatedAt: &now,
44+
ValidatedAt: &now,
45+
Provider: realProvider,
46+
}
47+
48+
t.Run("non-managed exposes location and provider verbatim", func(t *testing.T) {
49+
in := base
50+
in.Managed = false
51+
got := bizCASBackendToPb(&in)
52+
assert.Equal(t, realLocation, got.Location,
53+
"non-managed rows must surface their real location")
54+
assert.Equal(t, string(realProvider), got.Provider,
55+
"non-managed rows must surface their real provider")
56+
assert.False(t, got.IsManaged)
57+
})
58+
59+
t.Run("managed replaces location and provider with placeholders", func(t *testing.T) {
60+
in := base
61+
in.Managed = true
62+
got := bizCASBackendToPb(&in)
63+
assert.Equal(t, biz.CASBackendManagedLocationDisplay, got.Location,
64+
"managed rows must never leak the underlying AP ARN")
65+
assert.Equal(t, biz.CASBackendManagedProviderDisplay, got.Provider,
66+
"managed rows must never leak the backing provider ID")
67+
assert.True(t, got.IsManaged)
68+
})
69+
}

app/controlplane/pkg/biz/casbackend.go

Lines changed: 56 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,54 @@ const (
4848
MinCASBackendMaxBytes int64 = 10 * 1024 * 1024 // 10MB minimum
4949
errMsgCredentialsAccess = "Failed to access CAS backend credentials in external Secrets Manager"
5050
errMsgCredentialsFormat = "Invalid CAS backend credentials format from external Secrets Manager"
51+
52+
// CASBackendManagedLocationDisplay is the placeholder substituted for
53+
// CASBackend.Location whenever a managed backend is exposed beyond
54+
// the controlplane's trust boundary (API responses, audit events).
55+
// The real ARN remains in the DB so PerformValidation, the platform
56+
// reconciler, and forensic joins by CASBackendID still work — only
57+
// the wire-format Location is sanitized.
58+
CASBackendManagedLocationDisplay = "managed by Chainloop"
59+
60+
// CASBackendManagedProviderDisplay is the placeholder substituted
61+
// for CASBackend.Provider on managed backends. The underlying
62+
// provider ID ("AWS-S3-ACCESS-POINT" today, possibly other managed
63+
// providers tomorrow) is itself an implementation detail that
64+
// tenants shouldn't see; "Chainloop" tells them everything they
65+
// need to know about ownership without revealing the backing
66+
// technology.
67+
CASBackendManagedProviderDisplay = "Chainloop"
5168
)
5269

70+
// displayLocation returns the location string we expose outside the
71+
// controlplane's trust boundary. Managed backends get a stable
72+
// placeholder; everything else passes through verbatim. Use this for
73+
// any path that emits a CASBackend.Location to API clients or to the
74+
// audit event bus.
75+
func displayLocation(b *CASBackend) string {
76+
if b != nil && b.Managed {
77+
return CASBackendManagedLocationDisplay
78+
}
79+
if b == nil {
80+
return ""
81+
}
82+
return b.Location
83+
}
84+
85+
// displayProvider returns the provider string we expose outside the
86+
// controlplane's trust boundary. Managed backends report a generic
87+
// "Chainloop" provider name so the specific backing technology stays
88+
// internal. Non-managed backends pass through their provider ID.
89+
func displayProvider(b *CASBackend) string {
90+
if b == nil {
91+
return ""
92+
}
93+
if b.Managed {
94+
return CASBackendManagedProviderDisplay
95+
}
96+
return string(b.Provider)
97+
}
98+
5399
var CASBackendInlineDescription = "Embed artifacts content in the attestation (fallback)"
54100

55101
type CASBackendValidationStatus string
@@ -411,8 +457,8 @@ func (uc *CASBackendUseCase) Create(ctx context.Context, orgID, name, location,
411457
CASBackendBase: &events.CASBackendBase{
412458
CASBackendID: &backend.ID,
413459
CASBackendName: backend.Name,
414-
Provider: string(backend.Provider),
415-
Location: backend.Location,
460+
Provider: displayProvider(backend),
461+
Location: displayLocation(backend),
416462
Default: backend.Default,
417463
},
418464
CASBackendDescription: description,
@@ -533,8 +579,8 @@ func (uc *CASBackendUseCase) Update(ctx context.Context, orgID, id string, descr
533579
CASBackendBase: &events.CASBackendBase{
534580
CASBackendID: &after.ID,
535581
CASBackendName: after.Name,
536-
Provider: string(after.Provider),
537-
Location: after.Location,
582+
Provider: displayProvider(after),
583+
Location: displayLocation(after),
538584
Default: after.Default,
539585
},
540586
NewDescription: description,
@@ -643,8 +689,8 @@ func (uc *CASBackendUseCase) SoftDelete(ctx context.Context, orgID, id string) e
643689
CASBackendBase: &events.CASBackendBase{
644690
CASBackendID: &backend.ID,
645691
CASBackendName: backend.Name,
646-
Provider: string(backend.Provider),
647-
Location: backend.Location,
692+
Provider: displayProvider(backend),
693+
Location: displayLocation(backend),
648694
Default: backend.Default,
649695
},
650696
}, &orgUUID)
@@ -692,8 +738,8 @@ func (uc *CASBackendUseCase) Delete(ctx context.Context, id string) error {
692738
CASBackendBase: &events.CASBackendBase{
693739
CASBackendID: &backend.ID,
694740
CASBackendName: backend.Name,
695-
Provider: string(backend.Provider),
696-
Location: backend.Location,
741+
Provider: displayProvider(backend),
742+
Location: displayLocation(backend),
697743
Default: backend.Default,
698744
},
699745
}, &backend.OrganizationID)
@@ -782,8 +828,8 @@ func (uc *CASBackendUseCase) PerformValidation(ctx context.Context, id string) e
782828
CASBackendBase: &events.CASBackendBase{
783829
CASBackendID: &backend.ID,
784830
CASBackendName: backend.Name,
785-
Provider: string(backend.Provider),
786-
Location: backend.Location,
831+
Provider: displayProvider(backend),
832+
Location: displayLocation(backend),
787833
Default: backend.Default,
788834
},
789835
PreviousStatus: string(previousStatus),

0 commit comments

Comments
 (0)