Skip to content

Commit 5c08c98

Browse files
committed
feat(casbackend): reject managed-only providers in service Create
CASBackendService.Create previously accepted any provider ID present in the loader's provider map, including AWS-S3-ACCESS-POINT. A sufficiently determined user could craft a Create request that half-provisioned a managed row pointing at an AP ARN they don't own, bypassing the platform reconciler's trust boundary. Add an explicit isManagedOnlyProvider() guard at the front of Create so the public RPC fails fast with `managed CAS backends cannot be created via this API`. The platform reconciler still creates managed rows by calling biz.CASBackendUseCase.Create directly, which is unaffected. Update/SoftDelete are already guarded against managed rows in the biz layer. Assisted-by: Claude Code Signed-off-by: Jose I. Paris <jiparis@chainloop.dev> Chainloop-Trace-Sessions: 234a03ed-b238-4506-95f0-235242842db2
1 parent 0875185 commit 5c08c98

2 files changed

Lines changed: 46 additions & 0 deletions

File tree

app/controlplane/internal/service/casbackend.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1"
2222
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz"
2323
backend "github.com/chainloop-dev/chainloop/pkg/blobmanager"
24+
"github.com/chainloop-dev/chainloop/pkg/blobmanager/s3accesspoint"
2425
"github.com/go-kratos/kratos/v2/errors"
2526
"google.golang.org/protobuf/types/known/timestamppb"
2627
)
@@ -66,6 +67,16 @@ func (s *CASBackendService) Create(ctx context.Context, req *pb.CASBackendServic
6667
return nil, err
6768
}
6869

70+
// Managed-only providers (currently AWS-S3-ACCESS-POINT) are
71+
// reserved for the platform reconciler, which provisions them via
72+
// the biz layer directly. Reject any attempt to create one through
73+
// the public RPC so a user can't end up with a half-provisioned row
74+
// pointing at an AP they don't own.
75+
if isManagedOnlyProvider(req.Provider) {
76+
return nil, errors.BadRequest("invalid CAS backend",
77+
"managed CAS backends cannot be created via this API")
78+
}
79+
6980
backendP, ok := s.providers[req.Provider]
7081
if !ok {
7182
return nil, errors.BadRequest("invalid CAS backend", "invalid CAS backend")
@@ -237,3 +248,12 @@ func bizCASBackendToPb(in *biz.CASBackend) *pb.CASBackendItem {
237248

238249
return r
239250
}
251+
252+
// isManagedOnlyProvider returns true when the supplied provider ID can
253+
// only be instantiated by the platform reconciler — never by a user via
254+
// the public CASBackendService.Create RPC. Today that's just the S3
255+
// Access Point provider; if more managed providers land later, list
256+
// them here.
257+
func isManagedOnlyProvider(id string) bool {
258+
return id == s3accesspoint.ProviderID
259+
}

app/controlplane/internal/service/casbackend_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"time"
2121

2222
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz"
23+
"github.com/chainloop-dev/chainloop/pkg/blobmanager/s3accesspoint"
2324
"github.com/google/uuid"
2425
"github.com/stretchr/testify/assert"
2526
)
@@ -67,3 +68,28 @@ func TestBizCASBackendToPb_HidesManagedDetails(t *testing.T) {
6768
assert.True(t, got.IsManaged)
6869
})
6970
}
71+
72+
// TestIsManagedOnlyProvider locks down which provider IDs are reserved
73+
// for the platform reconciler. If a new managed provider is added but
74+
// this list isn't updated, users would be able to create the row
75+
// directly via CASBackendService.Create — a privilege escalation against
76+
// the managed-CAS trust model.
77+
func TestIsManagedOnlyProvider(t *testing.T) {
78+
tests := []struct {
79+
id string
80+
expected bool
81+
}{
82+
{s3accesspoint.ProviderID, true},
83+
{"AWS-S3", false},
84+
{"OCI", false},
85+
{"AzureBlob", false},
86+
{"INLINE", false},
87+
{"", false},
88+
{"unknown-provider", false},
89+
}
90+
for _, tc := range tests {
91+
t.Run(tc.id, func(t *testing.T) {
92+
assert.Equal(t, tc.expected, isManagedOnlyProvider(tc.id))
93+
})
94+
}
95+
}

0 commit comments

Comments
 (0)