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
2 changes: 1 addition & 1 deletion app/artifact-cas/api/cas/v1/status_http.pb.go

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

12 changes: 12 additions & 0 deletions app/controlplane/cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ import (
"github.com/chainloop-dev/chainloop/app/controlplane/internal/server"
"github.com/chainloop-dev/chainloop/app/controlplane/plugins"
"github.com/chainloop-dev/chainloop/app/controlplane/plugins/sdk/v1"
backends "github.com/chainloop-dev/chainloop/internal/blobmanager"
"github.com/chainloop-dev/chainloop/internal/blobmanager/oci"
"github.com/chainloop-dev/chainloop/internal/credentials"
credsConfig "github.com/chainloop-dev/chainloop/internal/credentials/api/credentials/v1"
"github.com/chainloop-dev/chainloop/internal/servicelogger"

Expand Down Expand Up @@ -167,6 +170,15 @@ func maskArgs(keyvals []interface{}) {
}
}

func loadCASBackendProviders(creader credentials.Reader) backends.Providers {
// Initialize CAS backend providers
// For now we only have OCI as a backend provider
p := oci.NewBackendProvider(creader)
return backends.Providers{
p.ID(): p,
}
}

func initSentry(c *conf.Bootstrap, logger log.Logger) (cleanupFunc func(), err error) {
cleanupFunc = func() {
sentry.Flush(2 * time.Second)
Expand Down
5 changes: 1 addition & 4 deletions app/controlplane/cmd/wire.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@ import (
"github.com/chainloop-dev/chainloop/app/controlplane/internal/server"
"github.com/chainloop-dev/chainloop/app/controlplane/internal/service"
"github.com/chainloop-dev/chainloop/app/controlplane/plugins/sdk/v1"
backend "github.com/chainloop-dev/chainloop/internal/blobmanager"
"github.com/chainloop-dev/chainloop/internal/blobmanager/oci"
"github.com/chainloop-dev/chainloop/internal/credentials"
"github.com/go-kratos/kratos/v2/log"
"github.com/google/wire"
Expand All @@ -42,10 +40,9 @@ func wireApp(*conf.Bootstrap, credentials.ReaderWriter, log.Logger, sdk.Availabl
server.ProviderSet,
data.ProviderSet,
biz.ProviderSet,
loadCASBackendProviders,
service.ProviderSet,
wire.Bind(new(backend.Provider), new(*oci.BackendProvider)),
wire.Bind(new(biz.CASClient), new(*biz.CASClientUseCase)),
oci.NewBackendProvider,
serviceOpts,
wire.Value([]biz.CASClientOpts{}),
wire.FieldsOf(new(*conf.Bootstrap), "Server", "Auth", "Data", "CasServer"),
Expand Down
7 changes: 3 additions & 4 deletions app/controlplane/cmd/wire_gen.go

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

124 changes: 41 additions & 83 deletions app/controlplane/internal/biz/casbackend.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,12 @@ package biz
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"time"

backend "github.com/chainloop-dev/chainloop/internal/blobmanager"
"github.com/chainloop-dev/chainloop/internal/blobmanager/oci"
"github.com/chainloop-dev/chainloop/internal/credentials"
"github.com/chainloop-dev/chainloop/internal/ociauth"
"github.com/chainloop-dev/chainloop/internal/servicelogger"
"github.com/go-kratos/kratos/v2/log"
"github.com/google/uuid"
Expand All @@ -47,7 +44,7 @@ type CASBackend struct {
ID uuid.UUID
Location, Description, SecretName string
CreatedAt, ValidatedAt *time.Time
OrganizationID string
OrganizationID uuid.UUID
ValidationStatus CASBackendValidationStatus
// OCI, S3, ...
Provider CASBackendProvider
Expand All @@ -56,14 +53,14 @@ type CASBackend struct {
}

type CASBackendOpts struct {
OrgID uuid.UUID
Location, SecretName, Description string
Provider CASBackendProvider
Default bool
}

type CASBackendCreateOpts struct {
*CASBackendOpts
OrgID uuid.UUID
}

type CASBackendUpdateOpts struct {
Expand All @@ -85,23 +82,23 @@ type CASBackendRepo interface {

type CASBackendReader interface {
FindDefaultBackend(ctx context.Context, orgID string) (*CASBackend, error)
FindByID(ctx context.Context, ID string) (*CASBackend, error)
FindByIDInOrg(ctx context.Context, OrgID, ID string) (*CASBackend, error)
PerformValidation(ctx context.Context, ID string) error
}

type CASBackendUseCase struct {
repo CASBackendRepo
logger *log.Helper
credsRW credentials.ReaderWriter
ociBackendProvider backend.Provider
repo CASBackendRepo
logger *log.Helper
credsRW credentials.ReaderWriter
providers backend.Providers
}

func NewCASBackendUseCase(repo CASBackendRepo, credsRW credentials.ReaderWriter, p backend.Provider, l log.Logger) *CASBackendUseCase {
func NewCASBackendUseCase(repo CASBackendRepo, credsRW credentials.ReaderWriter, providers backend.Providers, l log.Logger) *CASBackendUseCase {
if l == nil {
l = log.NewStdLogger(io.Discard)
}

return &CASBackendUseCase{repo, servicelogger.ScopedHelper(l, "biz/CASBackend"), credsRW, p}
return &CASBackendUseCase{repo, servicelogger.ScopedHelper(l, "biz/CASBackend"), credsRW, providers}
}

func (uc *CASBackendUseCase) List(ctx context.Context, orgID string) ([]*CASBackend, error) {
Expand All @@ -119,16 +116,7 @@ func (uc *CASBackendUseCase) FindDefaultBackend(ctx context.Context, orgID strin
return nil, NewErrInvalidUUID(err)
}

return uc.repo.FindDefaultBackend(ctx, orgUUID)
}

func (uc *CASBackendUseCase) FindByID(ctx context.Context, id string) (*CASBackend, error) {
backendUUID, err := uuid.Parse(id)
if err != nil {
return nil, NewErrInvalidUUID(err)
}

backend, err := uc.repo.FindByID(ctx, backendUUID)
backend, err := uc.repo.FindDefaultBackend(ctx, orgUUID)
if err != nil {
return nil, err
} else if backend == nil {
Expand All @@ -138,75 +126,49 @@ func (uc *CASBackendUseCase) FindByID(ctx context.Context, id string) (*CASBacke
return backend, nil
}

func validateAndExtractCredentials(provider CASBackendProvider, location string, credsJSON []byte) (any, error) {
// TODO: (miguel) this logic (marshalling from struct + validation) will be moved to the actual backend implementation
// This endpoint will support other backends in the future
if provider != CASBackendOCI {
return nil, NewErrValidation(errors.New("unsupported provider"))
}

var ociConfig = struct {
Password string `json:"password"`
Username string `json:"username"`
}{}

if err := json.Unmarshal(credsJSON, &ociConfig); err != nil {
return nil, NewErrValidation(err)
}

// Create and validate credentials
k, err := ociauth.NewCredentials(location, ociConfig.Username, ociConfig.Password)
func (uc *CASBackendUseCase) FindByIDInOrg(ctx context.Context, orgID, id string) (*CASBackend, error) {
orgUUID, err := uuid.Parse(orgID)
if err != nil {
return nil, NewErrValidation(err)
return nil, NewErrInvalidUUID(err)
}

// Check credentials
b, err := oci.NewBackend(location, &oci.RegistryOptions{Keychain: k})
backendUUID, err := uuid.Parse(id)
if err != nil {
return nil, fmt.Errorf("checking credentials: %w", err)
}

if err := b.CheckWritePermissions(context.TODO()); err != nil {
return nil, NewErrValidation(fmt.Errorf("wrong credentials: %w", err))
return nil, NewErrInvalidUUID(err)
}

// Validate and store the secret in the external secrets manager
creds := &credentials.OCIKeypair{Repo: location, Username: ociConfig.Username, Password: ociConfig.Password}
if err := creds.Validate(); err != nil {
return nil, NewErrValidation(err)
backend, err := uc.repo.FindByIDInOrg(ctx, orgUUID, backendUUID)
if err != nil {
return nil, err
} else if backend == nil {
return nil, NewErrNotFound("CAS Backend")
}

return creds, nil
return backend, nil
}

func (uc *CASBackendUseCase) Create(ctx context.Context, orgID, location, description string, provider CASBackendProvider, credsJSON []byte, defaultB bool) (*CASBackend, error) {
func (uc *CASBackendUseCase) Create(ctx context.Context, orgID, location, description string, provider CASBackendProvider, creds any, defaultB bool) (*CASBackend, error) {
orgUUID, err := uuid.Parse(orgID)
if err != nil {
return nil, NewErrInvalidUUID(err)
}

// Validate and store the secret in the external secrets manager
creds, err := validateAndExtractCredentials(provider, location, credsJSON)
if err != nil {
return nil, NewErrValidation(err)
}

secretName, err := uc.credsRW.SaveCredentials(ctx, orgID, creds)
if err != nil {
return nil, fmt.Errorf("storing the credentials: %w", err)
}

return uc.repo.Create(ctx, &CASBackendCreateOpts{
OrgID: orgUUID,
CASBackendOpts: &CASBackendOpts{
Location: location, SecretName: secretName, Provider: provider, Default: defaultB,
Description: description,
OrgID: orgUUID,
},
})
}

// Update will update credentials, description or default status
func (uc *CASBackendUseCase) Update(ctx context.Context, orgID, id, description string, credsJSON []byte, defaultB bool) (*CASBackend, error) {
func (uc *CASBackendUseCase) Update(ctx context.Context, orgID, id, description string, creds any, defaultB bool) (*CASBackend, error) {
orgUUID, err := uuid.Parse(orgID)
if err != nil {
return nil, NewErrInvalidUUID(err)
Expand All @@ -226,13 +188,7 @@ func (uc *CASBackendUseCase) Update(ctx context.Context, orgID, id, description

var secretName string
// We want to rotate credentials
if credsJSON != nil {
// Validate and store the secret in the external secrets manager
creds, err := validateAndExtractCredentials(repo.Provider, repo.Location, credsJSON)
if err != nil {
return nil, NewErrValidation(err)
}

if creds != nil {
secretName, err = uc.credsRW.SaveCredentials(ctx, orgID, creds)
if err != nil {
return nil, fmt.Errorf("storing the credentials: %w", err)
Expand All @@ -242,13 +198,11 @@ func (uc *CASBackendUseCase) Update(ctx context.Context, orgID, id, description
return uc.repo.Update(ctx, &CASBackendUpdateOpts{
ID: uuid,
CASBackendOpts: &CASBackendOpts{
SecretName: secretName, Default: defaultB, Description: description,
SecretName: secretName, Default: defaultB, Description: description, OrgID: orgUUID,
},
})
}

// TODO(miguel): we need to think about the update mechanism and add some guardrails
// for example, we might only allow updating credentials but not the repository itself or the provider
// Deprecated: use Create and update methods separately instead
func (uc *CASBackendUseCase) CreateOrUpdate(ctx context.Context, orgID, name, username, password string, provider CASBackendProvider, defaultB bool) (*CASBackend, error) {
orgUUID, err := uuid.Parse(orgID)
Expand Down Expand Up @@ -284,10 +238,10 @@ func (uc *CASBackendUseCase) CreateOrUpdate(ctx context.Context, orgID, name, us
}

return uc.repo.Create(ctx, &CASBackendCreateOpts{
OrgID: orgUUID,
CASBackendOpts: &CASBackendOpts{
Location: name, SecretName: secretName, Provider: provider,
Default: defaultB,
OrgID: orgUUID,
},
})
}
Expand Down Expand Up @@ -353,8 +307,6 @@ func (CASBackendValidationStatus) Values() (kinds []string) {
}

// Validate that the repository is valid and reachable
// TODO: run this process periodically in the background
// TODO: we need to support other kinds of repositories this is for the OCI type
func (uc *CASBackendUseCase) PerformValidation(ctx context.Context, id string) (err error) {
validationStatus := CASBackendValidationFailed

Expand All @@ -370,10 +322,9 @@ func (uc *CASBackendUseCase) PerformValidation(ctx context.Context, id string) (
return NewErrNotFound("CAS Backend")
}

// Currently this code is just for OCI repositories
if backend.Provider != CASBackendOCI {
uc.logger.Warnw("msg", "validation not supported for this provider", "ID", id, "provider", backend.Provider)
return nil
provider, ok := uc.providers[string(backend.Provider)]
if !ok {
return fmt.Errorf("CAS backend provider not found: %s", backend.Provider)
}

defer func() {
Expand All @@ -390,14 +341,21 @@ func (uc *CASBackendUseCase) PerformValidation(ctx context.Context, id string) (
}()

// 1 - Retrieve the credentials from the external secrets manager
b, err := uc.ociBackendProvider.FromCredentials(ctx, backend.SecretName)
if err != nil {
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)
return nil
}

// 2 - Perform a write validation
if err = b.CheckWritePermissions(context.TODO()); err != nil {
credsJSON, err := json.Marshal(creds)
if err != nil {
uc.logger.Infow("msg", "credentials invalid", "ID", id)
return nil
}

// 2 - run validation
_, err = provider.ValidateAndExtractCredentials(backend.Location, credsJSON)
if err != nil {
uc.logger.Infow("msg", "permissions validation failed", "ID", id)
return nil
}
Expand Down
Loading