Skip to content

Commit

Permalink
GBICSGO-1198: adds check for immutable backup sink
Browse files Browse the repository at this point in the history
  • Loading branch information
KaanTolunayKilicOG committed Apr 3, 2024
1 parent bfb6429 commit 9c210b4
Show file tree
Hide file tree
Showing 10 changed files with 374 additions and 1 deletion.
5 changes: 4 additions & 1 deletion cron.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,7 @@ cron:
schedule: every 15 minutes from 00:08 to 23:58
- description: "check app health status"
url: /_ah/health
schedule: every 1 minutes
schedule: every 1 minutes
- description: "check immutable sink data"
url: /api/tasks/check_immutable_backups
schedule: every 10 minutes from 00:12 to 23:52
31 changes: 31 additions & 0 deletions pkg/http/mock/fixtures.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ var (
TablePartitionResultHTTPMock = NewMockedHTTPRequest("GET", "/bigquery/v2/projects/.*/queries", getTablePartitionsQueryResponse)
// ExtractJobResultOkHTTPMock request
ExtractJobResultOkHTTPMock = NewMockedHTTPRequest("GET", "/bigquery/v2/projects/.*/jobs/.*", getExtractJobResultOkResponse)

ListPoliciesUnsafeHTTPMock = NewMockedHTTPRequest("GET", "policies/cloudresourcemanager.googleapis.com%252Fprojects%252Ftest-example-unsafe/denypolicies", emptyListPoliciesResultResponse)
ListPoliciesSafeHTTPMock = NewMockedHTTPRequest("GET", "policies/cloudresourcemanager.googleapis.com%252Fprojects%252Ftest-example-safe/denypolicies", listPoliciesResultResponse)
)

const (
Expand Down Expand Up @@ -186,6 +189,34 @@ Content-Type: application/json; charset=UTF-8
Content-Type: application/xml; charset=UTF-8
`
emptyListPoliciesResultResponse = `HTTP/1.1 200
Content-Type: application/json; charset=UTF-8
{}`

listPoliciesResultResponse = `HTTP/1.1 200
Content-Type: application/json; charset=UTF-8
{"policies": [{
"displayName": "My deny policy.",
"rules": [
{
"denyRule": {
"deniedPrincipals": [
"principalSet://goog/public:all"
],
"deniedPermissions": [
"storage.googleapis.com/objects.update",
"storage.googleapis.com/objects.delete",
"storage.googleapis.com/objects.create"
],
"exceptionPrincipals": [
"principal://iam.googleapis.com/projects/-/serviceAccounts/[email protected]"
]
}
}
]
}]}`
)

func SimpleResponseBodyFromTemplate(bodyTemplate string, values map[string]string, statusCode int) (string, error) {
Expand Down
53 changes: 53 additions & 0 deletions pkg/repository/backup_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ type BackupRepository interface {
GetExpiredBigQueryMirrorRevisions(ctxIn context.Context, maxRevisionLifetimeInWeeks int) ([]*MirrorRevision, error)
GetBigQueryOneShotSnapshots(ctxIn context.Context, status BackupStatus) ([]*Backup, error)
GetScheduledBackups(context.Context, BackupType) ([]*Backup, error)
ListBackupSinkProjects(ctx context.Context) ([]string, error)
MarkTargetSinksAsImmutable(ctx context.Context, sink string) error
MarkTargetSinksAsMutable(ctx context.Context, sink string) error
}

// defaultBackupRepository implements BackupRepository
Expand All @@ -53,6 +56,56 @@ type defaultBackupRepository struct {
ctx context.Context
}

func (d *defaultBackupRepository) MarkTargetSinksAsMutable(ctx context.Context, sink string) error {
_, span := trace.StartSpan(ctx, "(*defaultBackupRepository).MarkTargetSinksAsMutable")
defer span.End()

return d.markTargetSinkAsImmutable(ctx, sink, false)
}

func (d *defaultBackupRepository) MarkTargetSinksAsImmutable(ctx context.Context, sink string) error {
_, span := trace.StartSpan(ctx, "(*defaultBackupRepository).MarkTargetSinksAsImmutable")
defer span.End()

return d.markTargetSinkAsImmutable(ctx, sink, true)
}

func (d *defaultBackupRepository) markTargetSinkAsImmutable(ctx context.Context, sink string, isImmutable bool) error {
_, span := trace.StartSpan(ctx, "(*defaultBackupRepository).markTargetSinkAsImmutable")
defer span.End()

_, err := d.storageService.DB().
Model(&Backup{}).
Set("sink_is_immutable = ?", isImmutable).
Where("target_sink = ?", sink).
Update()

if err != nil {
logQueryError("markTargetSinkAsImmutable", err)
return fmt.Errorf("error during executing updating backup statemant: %s", err)
}

return nil
}

func (d *defaultBackupRepository) ListBackupSinkProjects(ctx context.Context) ([]string, error) {
_, span := trace.StartSpan(ctx, "(*defaultBackupRepository).ListBackupSinkProjects")
defer span.End()

var projects []string
err := d.storageService.
DB().
Model(&Backup{}).
ColumnExpr("DISTINCT target_sink").
Where("audit_deleted_timestamp IS NULL").
Select(&projects)
if err != nil {
logQueryError("ListBackupSinkProjects", err)
return nil, fmt.Errorf("error during executing get backup by status statement: %s", err)
}
return projects, nil
}

// NewBackupRepository return instance of BackupRepository
func NewBackupRepository(ctxIn context.Context, credentialsProvider secret.SecretProvider) (BackupRepository, error) {
ctx, span := trace.StartSpan(ctxIn, "NewBackupRepository")
Expand Down
2 changes: 2 additions & 0 deletions pkg/repository/entities.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ type Backup struct {
LastScheduledTime time.Time `pg:"last_scheduled_timestamp"`
LastCleanupTime time.Time `pg:"last_cleanup_timestamp"`

SinkIsImmutable bool `pg:"sink_is_immutable"`

SinkOptions
SnapshotOptions
BackupOptions
Expand Down
26 changes: 26 additions & 0 deletions pkg/repository/memory/backup_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,32 @@ type BackupRepository struct {
backups []*repository.Backup
}

func (r *BackupRepository) ListBackupSinkProjects(ctx context.Context) ([]string, error) {
var targetSinks []string
for _, backup := range r.backups {
targetSinks = append(targetSinks, backup.SinkOptions.TargetProject)
}
return targetSinks, nil
}

func (r *BackupRepository) MarkTargetSinksAsImmutable(ctx context.Context, sink string) error {
for _, backup := range r.backups {
if backup.SinkOptions.TargetProject == sink {
backup.SinkIsImmutable = true
}
}
return nil
}

func (r *BackupRepository) MarkTargetSinksAsMutable(ctx context.Context, sink string) error {
for _, backup := range r.backups {
if backup.SinkOptions.TargetProject == sink {
backup.SinkIsImmutable = false
}
}
return nil
}

// UpdateBackupStatus is not implemented
func (r *BackupRepository) UpdateBackup(ctxIn context.Context, updateFields repository.UpdateFields) error {
_, span := trace.StartSpan(ctxIn, "(*BackupRepository).UpdateBackupStatus")
Expand Down
176 changes: 176 additions & 0 deletions pkg/tasks/check_immutable_backups_service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package tasks

import (
iam "cloud.google.com/go/iam/apiv2"
"cloud.google.com/go/iam/apiv2/iampb"
"context"
"errors"
"fmt"
"github.com/golang/glog"
"github.com/ottogroup/penelope/pkg/http/impersonate"
"github.com/ottogroup/penelope/pkg/repository"
"github.com/ottogroup/penelope/pkg/secret"
"go.opencensus.io/trace"
gimpersonate "google.golang.org/api/impersonate"
"google.golang.org/api/iterator"
"google.golang.org/api/option"
"net/http"
"strings"
)

type checkImmutableBackupsService struct {
backupRepository repository.BackupRepository
tokenSourceProvider impersonate.TargetPrincipalForProjectProvider
}

var cloudStorageEditPermissions = []string{
"storage.googleapis.com/objects.update",
"storage.googleapis.com/objects.delete",
"storage.googleapis.com/objects.create",
}

const (
allPrincipals = "principalSet://goog/public:all"
)

func newCheckImmutableBackupsService(ctxIn context.Context, tokenSourceProvider impersonate.TargetPrincipalForProjectProvider, credentialsProvider secret.SecretProvider) (*checkImmutableBackupsService, error) {
ctx, span := trace.StartSpan(ctxIn, "newCheckImmutableBackupsService")
defer span.End()

backupRepository, err := repository.NewBackupRepository(ctx, credentialsProvider)
if err != nil {
return nil, err
}

return &checkImmutableBackupsService{
backupRepository: backupRepository,
tokenSourceProvider: tokenSourceProvider,
}, nil
}

func (c *checkImmutableBackupsService) Run(ctxIn context.Context) {
ctx, span := trace.StartSpan(ctxIn, "(*checkImmutableBackupsService).Run")
defer span.End()

sinkProjects, err := c.backupRepository.ListBackupSinkProjects(ctx)
if err != nil {
glog.Error("could not get list of backups: %s", err)
return
}

for _, sink := range sinkProjects {
var isImmutable = false

targetPrincipal, delegates, err := c.tokenSourceProvider.GetTargetPrincipalForProject(ctx, sink)
if err != nil {
glog.Errorf("could not get target principal for project %s: %s", sink, err)
continue
}

tokenSource, err := gimpersonate.CredentialsTokenSource(ctx, gimpersonate.CredentialsConfig{
TargetPrincipal: targetPrincipal,
Scopes: []string{"https://www.googleapis.com/auth/cloud-platform"},
Delegates: delegates,
})
if err != nil {
glog.Errorf("could not create token source: %s", err)
return
}

options := []option.ClientOption{
option.WithTokenSource(tokenSource),
option.WithHTTPClient(http.DefaultClient),
}

policiesClient, err := iam.NewPoliciesRESTClient(ctx, options...)
if err != nil {
glog.Errorf("could not create new IAM policies client: %s", err)
}

// FIXME: does not show inherited deny policies
// bucket level deny policy is not supported: https://cloud.google.com/iam/docs/deny-access#attachment-point
attachmentPoint := fmt.Sprintf("cloudresourcemanager.googleapis.com%%2Fprojects%%2F%s", sink)

it := policiesClient.ListPolicies(ctx, &iampb.ListPoliciesRequest{
Parent: fmt.Sprintf("policies/%s/denypolicies", attachmentPoint),
})

for {
policy, err := it.Next()
if errors.Is(err, iterator.Done) {
break
}
if err != nil {
glog.Errorf("could not get next policy: %s", err)
break
}

/**
* We need to check if deny edit permission for cloud storage is set for all principals except for the
* target backup service account.
*/
for _, rule := range policy.Rules {
deniedPermissions := rule.GetDenyRule().GetDeniedPermissions()
deniedPrincipals := rule.GetDenyRule().GetDeniedPrincipals()
exceptionPrincipals := rule.GetDenyRule().GetExceptionPrincipals()

if !containsAllEditPermissions(deniedPermissions) {
continue
}

if !containsAllPrincipals(deniedPrincipals) {
continue
}

if !containsOnlyBackupServiceAccountAsException(targetPrincipal, exceptionPrincipals) {
continue
}

isImmutable = true
}
}

if err := policiesClient.Close(); err != nil {
glog.Errorf("could not close IAM policies client: %s", err)
}

if isImmutable {
err = c.backupRepository.MarkTargetSinksAsImmutable(ctx, sink)
} else {
err = c.backupRepository.MarkTargetSinksAsMutable(ctx, sink)
}

if err != nil {
glog.Errorf("could not mark target sink %s as safe: %s", sink, err)
}
}
}

func containsOnlyBackupServiceAccountAsException(targetPrincipal string, principals []string) bool {
return len(principals) == 1 && strings.EqualFold(principals[0], fmt.Sprintf("principal://iam.googleapis.com/projects/-/serviceAccounts/%s", targetPrincipal))
}

func containsAllPrincipals(principals []string) bool {
for _, item := range principals {
if strings.EqualFold(item, allPrincipals) {
return true
}
}
return false
}

func containsAllEditPermissions(permissions []string) bool {
for _, item := range cloudStorageEditPermissions {
found := false
for _, element := range permissions {
if item == element {
found = true
break
}
}
if !found {
return false
}
}
return true
}
Loading

0 comments on commit 9c210b4

Please sign in to comment.