Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 11 additions & 8 deletions app/cli/cmd/casbackend_list.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright 2024 The Chainloop Authors.
// Copyright 2024-2025 The Chainloop Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand All @@ -17,6 +17,7 @@ package cmd

import (
"fmt"
"strings"
"time"

"code.cloudfoundry.org/bytefmt"
Expand Down Expand Up @@ -56,9 +57,9 @@ func casBackendListTableOutput(backends []*action.CASBackendItem) error {
}

t := newTableWriter()
header := table.Row{"Name", "Location", "Provider", "Description", "Limits", "Default"}
header := table.Row{"Name", "Location", "Provider", "Description", "Limits", "Default", "Status"}
if full {
header = append(header, "Validation Status", "Created At", "Validated At")
header = append(header, "Created At", "Validated At")
}

t.AppendHeader(header)
Expand All @@ -68,12 +69,14 @@ func casBackendListTableOutput(backends []*action.CASBackendItem) error {
limits = fmt.Sprintf("MaxSize: %s", bytefmt.ByteSize(uint64(b.Limits.MaxBytes)))
}

r := table.Row{b.Name, wrap.String(b.Location, 35), b.Provider, wrap.String(b.Description, 35), limits, b.Default}
validationStatus := string(b.ValidationStatus)
if b.ValidationError != nil && *b.ValidationError != "" {
validationStatus = strings.Join([]string{validationStatus, wrap.String(*b.ValidationError, 50)}, "\n")
}

r := table.Row{b.Name, wrap.String(b.Location, 35), b.Provider, wrap.String(b.Description, 35), limits, b.Default, validationStatus}
if full {
r = append(r, b.ValidationStatus,
b.CreatedAt.Format(time.RFC822),
b.ValidatedAt.Format(time.RFC822),
)
r = append(r, b.CreatedAt.Format(time.RFC822), b.ValidatedAt.Format(time.RFC822))
}

t.AppendRow(r)
Expand Down
7 changes: 6 additions & 1 deletion app/cli/internal/action/casbackend_list.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright 2023 The Chainloop Authors.
// Copyright 2023-2025 The Chainloop Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -36,6 +36,7 @@ type CASBackendItem struct {
Inline bool `json:"inline"`
Limits *CASBackendLimits `json:"limits"`
ValidationStatus ValidationStatus `json:"validationStatus"`
ValidationError *string `json:"validationError,omitempty"`

CreatedAt *time.Time `json:"createdAt"`
ValidatedAt *time.Time `json:"validatedAt"`
Expand Down Expand Up @@ -102,5 +103,9 @@ func pbCASBackendItemToAction(in *pb.CASBackendItem) *CASBackendItem {
b.ValidationStatus = Invalid
}

if in.ValidationError != nil {
b.ValidationError = in.ValidationError
}

return b
}
194 changes: 104 additions & 90 deletions app/controlplane/api/controlplane/v1/response_messages.pb.go

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions app/controlplane/api/controlplane/v1/response_messages.proto
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,8 @@ message CASBackendItem {
// Is it an inline backend?
// inline means that the content is stored in the attestation itself
bool is_inline = 10;
// Error message if validation failed
optional string validation_error = 12;

message Limits {
// Max number of bytes allowed to be stored in this backend
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions app/controlplane/internal/service/casbackend.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,5 +176,9 @@ func bizCASBackendToPb(in *biz.CASBackend) *pb.CASBackendItem {
r.ValidationStatus = pb.CASBackendItem_VALIDATION_STATUS_INVALID
}

if in.ValidationError != nil {
r.ValidationError = in.ValidationError
}

return r
}
1 change: 1 addition & 0 deletions app/controlplane/pkg/auditor/events/casbackend.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ type CASBackendStatusChanged struct {
*CASBackendBase
PreviousStatus string `json:"previous_status,omitempty"`
NewStatus string `json:"new_status,omitempty"`
StatusError string `json:"status_error,omitempty"`
IsRecovery bool `json:"is_recovery,omitempty"`
}

Expand Down
35 changes: 25 additions & 10 deletions app/controlplane/pkg/biz/casbackend.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ type CASBackend struct {
CreatedAt, ValidatedAt *time.Time
OrganizationID uuid.UUID
ValidationStatus CASBackendValidationStatus
ValidationError *string
// OCI, S3, ...
Provider CASBackendProvider
// Whether this is the default cas backend for the organization
Expand All @@ -77,6 +78,8 @@ type CASBackendOpts struct {
Location, SecretName, Description string
Provider CASBackendProvider
Default bool
ValidationStatus CASBackendValidationStatus
ValidationError *string
}

type CASBackendCreateOpts struct {
Expand All @@ -98,10 +101,10 @@ type CASBackendRepo interface {
FindByIDInOrg(ctx context.Context, OrgID, ID uuid.UUID) (*CASBackend, error)
FindByNameInOrg(ctx context.Context, OrgID uuid.UUID, name string) (*CASBackend, error)
List(ctx context.Context, orgID uuid.UUID) ([]*CASBackend, error)
UpdateValidationStatus(ctx context.Context, ID uuid.UUID, status CASBackendValidationStatus, validationError *string) error
// ListBackends returns CAS backends across all organizations
// If onlyDefaults is true, only default backends are returned
ListBackends(ctx context.Context, onlyDefaults bool) ([]*CASBackend, error)
UpdateValidationStatus(ctx context.Context, ID uuid.UUID, status CASBackendValidationStatus) error
Create(context.Context, *CASBackendCreateOpts) (*CASBackend, error)
Update(context.Context, *CASBackendUpdateOpts) (*CASBackend, error)
Delete(ctx context.Context, ID uuid.UUID) error
Expand Down Expand Up @@ -345,6 +348,8 @@ func (uc *CASBackendUseCase) Update(ctx context.Context, orgID, id, description
ID: uuid,
CASBackendOpts: &CASBackendOpts{
SecretName: secretName, Default: defaultB, Description: description, OrgID: orgUUID,
ValidationStatus: CASBackendValidationOK,
ValidationError: ToPtr(""),
},
})
if err != nil {
Expand Down Expand Up @@ -530,6 +535,7 @@ func (CASBackendValidationStatus) Values() (kinds []string) {
// Validate that the repository is valid and reachable
func (uc *CASBackendUseCase) PerformValidation(ctx context.Context, id string) (err error) {
validationStatus := CASBackendValidationFailed
var validationError *string

backendUUID, err := uuid.Parse(id)
if err != nil {
Expand Down Expand Up @@ -559,16 +565,16 @@ func (uc *CASBackendUseCase) PerformValidation(ctx context.Context, id string) (
return
}

// Store previous status for audit logging
previousStatus := backend.ValidationStatus

// Update the validation status
uc.logger.Infow("msg", "updating validation status", "ID", id, "status", validationStatus)
if err := uc.repo.UpdateValidationStatus(ctx, backendUUID, validationStatus); err != nil {
// Update the validation status and error
uc.logger.Infow("msg", "updating validation status", "ID", id, "status", validationStatus, "error", validationError)
if err := uc.repo.UpdateValidationStatus(ctx, backendUUID, validationStatus, validationError); err != nil {
uc.logger.Errorw("msg", "updating validation status", "ID", id, "error", err)
return
}

// Store previous status for audit logging
previousStatus := backend.ValidationStatus

// Log status change as an audit event if status has changed and auditor is available
if uc.auditorUC != nil && previousStatus != validationStatus {
uc.logger.Debugw("msg", "status changed, dispatching audit event",
Expand All @@ -579,6 +585,11 @@ func (uc *CASBackendUseCase) PerformValidation(ctx context.Context, id string) (
// Check if this is a recovery event (going from failed to OK)
isRecovery := previousStatus == CASBackendValidationFailed && validationStatus == CASBackendValidationOK

var validationErrorStr string
if validationError != nil {
validationErrorStr = *validationError
}

// Create and send event for the status change
uc.auditorUC.Dispatch(ctx, &events.CASBackendStatusChanged{
CASBackendBase: &events.CASBackendBase{
Expand All @@ -590,6 +601,7 @@ func (uc *CASBackendUseCase) PerformValidation(ctx context.Context, id string) (
},
PreviousStatus: string(previousStatus),
NewStatus: string(validationStatus),
StatusError: validationErrorStr,
IsRecovery: isRecovery,
}, &backend.OrganizationID)
}
Expand All @@ -598,25 +610,28 @@ func (uc *CASBackendUseCase) PerformValidation(ctx context.Context, id string) (
// 1 - Retrieve the credentials from the external secrets manager
var creds any
if err := uc.credsRW.ReadCredentials(ctx, backend.SecretName, &creds); err != nil {
uc.logger.Infow("msg", "credentials not found or invalid", "ID", id)
uc.logger.Infow("msg", "credentials not found or invalid", "ID", id, "error", err)
return nil
}

credsJSON, err := json.Marshal(creds)
if err != nil {
uc.logger.Infow("msg", "credentials invalid", "ID", id)
uc.logger.Infow("msg", "credentials invalid", "ID", id, "error", err)
return nil
}

// 2 - run validation
_, err = provider.ValidateAndExtractCredentials(backend.Location, credsJSON)
if err != nil {
uc.logger.Infow("msg", "permissions validation failed", "ID", id)
errMsg := err.Error()
validationError = &errMsg
uc.logger.Infow("msg", "permissions validation failed", "ID", id, "error", err)
return nil
}

// If everything went well, update the validation status to OK
validationStatus = CASBackendValidationOK
validationError = nil
uc.logger.Infow("msg", "validation OK", "ID", id)

return nil
Expand Down
6 changes: 3 additions & 3 deletions app/controlplane/pkg/biz/casbackend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ func (s *casBackendTestSuite) TestPerformValidation() {

t.Run("proper provider credentials missing, set validation status => invalid", func(_ *testing.T) {
s.repo.On("FindByID", mock.Anything, s.validUUID).Return(validRepo, nil)
s.repo.On("UpdateValidationStatus", mock.Anything, s.validUUID, biz.CASBackendValidationFailed).Return(nil)
s.repo.On("UpdateValidationStatus", mock.Anything, s.validUUID, biz.CASBackendValidationFailed, mock.Anything).Return(nil)

s.credsRW.On("ReadCredentials", mock.Anything, mock.Anything, mock.Anything).Return(credentials.ErrNotFound)
err := s.useCase.PerformValidation(context.Background(), s.validUUID.String())
Expand All @@ -149,7 +149,7 @@ func (s *casBackendTestSuite) TestPerformValidation() {

t.Run("invalid credentials, set validation status => invalid", func(_ *testing.T) {
s.repo.On("FindByID", mock.Anything, s.validUUID).Return(validRepo, nil)
s.repo.On("UpdateValidationStatus", mock.Anything, s.validUUID, biz.CASBackendValidationFailed).Return(nil)
s.repo.On("UpdateValidationStatus", mock.Anything, s.validUUID, biz.CASBackendValidationFailed, mock.Anything).Return(nil)
s.credsRW.On("ReadCredentials", mock.Anything, mock.Anything, mock.Anything).Return(nil)
s.backendProvider.On("ValidateAndExtractCredentials", validRepo.Location, mock.Anything).Return(nil, errors.New("invalid credentials"))

Expand All @@ -160,7 +160,7 @@ func (s *casBackendTestSuite) TestPerformValidation() {

t.Run("valid credentials, set validation status => ok", func(_ *testing.T) {
s.repo.On("FindByID", mock.Anything, s.validUUID).Return(validRepo, nil)
s.repo.On("UpdateValidationStatus", mock.Anything, s.validUUID, biz.CASBackendValidationOK).Return(nil)
s.repo.On("UpdateValidationStatus", mock.Anything, s.validUUID, biz.CASBackendValidationOK, mock.Anything).Return(nil)
s.credsRW.On("ReadCredentials", mock.Anything, mock.Anything, mock.Anything).Return(nil)
s.backendProvider.On("ValidateAndExtractCredentials", validRepo.Location, mock.Anything).Return(nil, nil)

Expand Down
Loading
Loading