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
39 changes: 38 additions & 1 deletion app/controlplane/plugins/core/guac/v1/guac.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright 2023 The Chainloop Authors.
// Copyright 2023-2026 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 @@ -80,6 +80,31 @@ func New(l log.Logger) (sdk.FanOut, error) {
return &Integration{base}, err
}

// gcsServiceAccountType is the only Google credential type GUAC accepts.
const gcsServiceAccountType = "service_account"

// validateGCSCredentials checks that the supplied credentials are a Google
// service account key, the only type GUAC supports, and rejects anything else.
//
// The Google SDK dispatches on the credential "type" field and handles each
// type differently when it builds the client. GUAC only ever needs a service
// account key to write to a bucket, so the type is checked up front and every
// other type is refused, keeping the credential path narrow and predictable.
func validateGCSCredentials(raw string) error {
var parsed struct {
Type string `json:"type"`
}
if err := json.Unmarshal([]byte(raw), &parsed); err != nil {
return fmt.Errorf("credentials are not valid JSON: %w", err)
}

if parsed.Type != gcsServiceAccountType {
return fmt.Errorf("unsupported Google credential type %q: only a service account key is accepted", parsed.Type)
}

return nil
}

// Register is executed when a operator wants to register a specific instance of this integration with their Chainloop organization
func (i *Integration) Register(ctx context.Context, req *sdk.RegistrationRequest) (*sdk.RegistrationResponse, error) {
i.Logger.Info("registration requested")
Expand All @@ -90,6 +115,12 @@ func (i *Integration) Register(ctx context.Context, req *sdk.RegistrationRequest
return nil, fmt.Errorf("invalid registration request: %w", err)
}

// GUAC supports only a service account key. Validate the credential type
// before the Google SDK builds a client from it.
if err := validateGCSCredentials(request.Credentials); err != nil {
return nil, fmt.Errorf("invalid credentials: %w", err)
}

// Check that the credentials are valid and the bucket exists
client, err := storage.NewClient(ctx, option.WithCredentialsJSON([]byte(request.Credentials)))
if err != nil {
Expand Down Expand Up @@ -136,6 +167,12 @@ func (i *Integration) Execute(ctx context.Context, req *sdk.ExecutionRequest) er
return errors.New("missing expected credentials")
}

// Re-check the stored credential on every dispatch, so a registration created
// before this validation is refused here rather than passed to the SDK as-is.
if err := validateGCSCredentials(req.RegistrationInfo.Credentials.Password); err != nil {
return fmt.Errorf("invalid credentials: %w", err)
}

client, err := storage.NewClient(ctx, option.WithCredentialsJSON([]byte(req.RegistrationInfo.Credentials.Password)))
if err != nil {
return fmt.Errorf("creating storage client: %w", err)
Expand Down
86 changes: 86 additions & 0 deletions app/controlplane/plugins/core/guac/v1/guac_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,93 @@ func (s *testSuite) TearDownTest() {
s.server.Stop()
}

// TestRegisterRejectsNonServiceAccountCredentials proves the credential type is
// checked before the Google SDK is built: a non-service-account credential is
// refused by Register with no client created.
func (s *testSuite) TestRegisterRejectsNonServiceAccountCredentials() {
integration, err := New(log.NewStdLogger(io.Discard))
require.NoError(s.T(), err)

payload, err := sdk.ToConfig(&registrationRequest{
Bucket: "some-bucket",
Credentials: `{"type":"external_account"}`,
})
require.NoError(s.T(), err)

_, err = integration.(*Integration).Register(context.Background(), &sdk.RegistrationRequest{Payload: payload})
require.Error(s.T(), err)
assert.ErrorContains(s.T(), err, "invalid credentials")
assert.ErrorContains(s.T(), err, "service account key")
}

// Run all the tests
func TestGuacIntegration(t *testing.T) {
suite.Run(t, new(testSuite))
}

func TestValidateGCSCredentials(t *testing.T) {
const wantUnsupportedType = "unsupported Google credential type"

testCases := []struct {
name string
creds string
wantErr string // substring to expect; empty means the credential is accepted
}{
{
name: "service account key is accepted",
creds: `{"type":"service_account","project_id":"p"}`,
},
{
name: "external account is rejected",
creds: `{"type":"external_account"}`,
wantErr: wantUnsupportedType,
},
{
name: "impersonated service account is rejected",
creds: `{"type":"impersonated_service_account"}`,
wantErr: wantUnsupportedType,
},
{
name: "external account authorized user is rejected",
creds: `{"type":"external_account_authorized_user"}`,
wantErr: wantUnsupportedType,
},
{
name: "authorized user is rejected",
creds: `{"type":"authorized_user"}`,
wantErr: wantUnsupportedType,
},
{
name: "gdch service account is rejected",
creds: `{"type":"gdch_service_account"}`,
wantErr: wantUnsupportedType,
},
{
name: "missing type is rejected",
creds: `{"project_id":"p"}`,
wantErr: wantUnsupportedType,
},
{
name: "non-JSON is rejected",
creds: `-----BEGIN PRIVATE KEY-----`,
wantErr: "credentials are not valid JSON",
},
{
name: "empty string is rejected",
creds: ``,
wantErr: "credentials are not valid JSON",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := validateGCSCredentials(tc.creds)
if tc.wantErr == "" {
assert.NoError(t, err)
return
}
require.Error(t, err)
assert.Contains(t, err.Error(), tc.wantErr)
})
}
}
Loading