Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Principle of Least Privilege. Solve deprecation in logging client. #317

Merged
merged 10 commits into from
Apr 25, 2024
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
406 changes: 285 additions & 121 deletions Readme.md

Large diffs are not rendered by default.

Binary file added frontend/dist/penelope_48.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="/favicon.png" />
<link rel="icon" href="/penelope_48.png" />
<title>Penelope</title>
</head>

Expand Down
Binary file added frontend/public/Penelope.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/public/Penelope_250.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/public/penelope_48.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion frontend/src/App.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import logo from "@/assets/penelope_32.png";
import logo from "@/assets/penelope_48.png";
import Sandbox from "@/views/Sandbox.vue";
import { ref } from "vue";

Expand Down
Binary file added frontend/src/assets/penelope_48.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 6 additions & 1 deletion frontend/src/components/BackupCreateDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,12 @@ const apiRequestBody = () => {
recovery_time_objective: Number(request.value.recovery_time_objective),
type: request.value.type,
strategy: request.value.strategy,
target: request.value.target,
target: {
storage_class: request.value.target?.storage_class,
region: request.value.target?.region,
dual_region: request.value.target?.dual_region,
archive_ttm: Number(request.value.target?.archive_ttm),
},
snapshot_options: {
lifetime_in_days: Number(request.value.snapshot_options?.lifetime_in_days),
frequency_in_hours: Number(request.value.snapshot_options?.frequency_in_hours),
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/components/BackupTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -88,21 +88,21 @@ const projectLink = (project: string) => {
<template #[`item.source`]="{ item }">
<template v-if="item.type === BackupType.BIG_QUERY">
BigQuery:
<a :href="bigqueryDatasetLink(item.project ?? '', item.bigquery_options?.dataset ?? '')">{{
<a :href="bigqueryDatasetLink(item.project ?? '', item.bigquery_options?.dataset ?? '')" target="_blank">{{
item.bigquery_options?.dataset
}}</a>
<ul>
<li v-for="table in item.bigquery_options?.table">
Table:
<a :href="bigqueryTableLink(item.project ?? '', item.bigquery_options?.dataset ?? '', table)">{{
<a :href="bigqueryTableLink(item.project ?? '', item.bigquery_options?.dataset ?? '', table)" target="_blank">{{
table
}}</a>
</li>
</ul>
</template>
<template v-if="item.type === BackupType.CLOUD_STORAGE">
Bucket:
<a :href="cloudStorageLink(item.project ?? '', item.gcs_options?.bucket ?? '')">{{
<a :href="cloudStorageLink(item.project ?? '', item.gcs_options?.bucket ?? '')" target="_blank">{{
item.gcs_options?.bucket
}}</a>
</template>
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ require (
cloud.google.com/go/iam v1.1.7
cloud.google.com/go/logging v1.9.0
cloud.google.com/go/monitoring v1.18.2
cloud.google.com/go/resourcemanager v1.9.6
cloud.google.com/go/storage v1.40.0
contrib.go.opencensus.io/exporter/stackdriver v0.13.14
github.com/aws/aws-sdk-go v1.51.23
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ cloud.google.com/go/longrunning v0.5.6 h1:xAe8+0YaWoCKr9t1+aWe+OeQgN/iJK1fEgZSXm
cloud.google.com/go/longrunning v0.5.6/go.mod h1:vUaDrWYOMKRuhiv6JBnn49YxCPz2Ayn9GqyjaBT8/mA=
cloud.google.com/go/monitoring v1.18.2 h1:nIQdZdf1/9M0cEmXSlLB2jMq/k3CRh9p3oUzS06VDG8=
cloud.google.com/go/monitoring v1.18.2/go.mod h1:MuL95M6d9HtXQOaWP9JxhFZJKP+fdTF0Gt5xl4IDsew=
cloud.google.com/go/resourcemanager v1.9.6 h1:VPfJFbWxrTYQzEXCDbJNpcvSB8eZhTSM0YHH146fIB8=
cloud.google.com/go/resourcemanager v1.9.6/go.mod h1:d+XUOGbxg6Aka3lmC4fDiserslux3d15uX08C6a0MBg=
cloud.google.com/go/storage v1.40.0 h1:VEpDQV5CJxFmJ6ueWNsKxcr1QAYOXEgxDa+sBbJahPw=
cloud.google.com/go/storage v1.40.0/go.mod h1:Rrj7/hKlG87BLqDJYtwR0fbPld8uJPbQ2ucUMY7Ir0g=
cloud.google.com/go/trace v1.10.6 h1:XF0Ejdw0NpRfAvuZUeQe3ClAG4R/9w5JYICo7l2weaw=
Expand Down
3 changes: 0 additions & 3 deletions pkg/http/actions/updating.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,5 @@ func (dl *UpdateBackupHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
prepareResponse(w, logMsg, respMsg, http.StatusBadRequest)
return
}
if !checkRecoveryPointsAreValid(w, request.RecoveryPointObjective, request.RecoveryTimeObjective) {
return
}
handleRequestByProcessor(ctx, w, r, request, http.StatusOK, dl.processorBuilder.ProcessorForUpdating)
}
45 changes: 44 additions & 1 deletion pkg/http/mock/fixtures.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ var (
// BucketAttrsHTTPMock request
BucketAttrsHTTPMock = NewMockedHTTPRequest("GET", "/storage/v1/b/.*", bucketAttrsResponse)
// PatchBucketAttrsHTTPMock request
PatchBucketAttrsHTTPMock = NewMockedHTTPRequest("PATCH", "/storage/v1/b/.*", patchBucketAttrsResponse)
PatchBucketAttrsHTTPMock = NewMockedHTTPRequest("PATCH", "/storage/v1/b/.*", patchBucketAttrsResponse)
BucketSetIAMPolicyHTTPMock = NewMockedHTTPRequest("POST", "/storage/v1/b/.*/iam", bucketSetIAMPolicy)
// SinkCreatedHTTPpMock request
SinkCreatedHTTPpMock = NewMockedHTTPRequest("POST", "/storage/v1/b", sinkCreatedResponse)
// SinkDeletedHTTPMock request
Expand Down Expand Up @@ -60,6 +61,7 @@ var (
ListPoliciesSafeHTTPMock = NewMockedHTTPRequest("GET", "policies/cloudresourcemanager.googleapis.com%252Fprojects%252Ftest-example-safe/denypolicies", listPoliciesResultResponse)

ListServiceUsageHTTPMock = NewMockedHTTPRequest("GET", "projects/.*/services", listServiceUsageOkResponse)
GetPRojectHTTPMock = NewMockedHTTPRequest("GET", "/v3/projects/.*/", getProjectOkResponse)
)

const (
Expand Down Expand Up @@ -201,6 +203,47 @@ Content-Type: application/json; charset=UTF-8

{"services": [],"nextPageToken": null}`

getProjectOkResponse = `HTTP/1.1 200
Content-Type: application/json; charset=UTF-8

{
"name": "projects/00000000000",
"parent": "folders/00000000001",
"projectId": "project1",
"state": "ACTIVE",
"displayName": "project1",
"createTime": "2021-03-12T21:16:05.027Z",
"updateTime": "2023-07-05T07:58:52.363128Z",
"etag": "W/\"tagValue\"",
"labels": {
"env": "dev",
"service_owner": "[email protected]",
"team": "team1"
}
}`
bucketSetIAMPolicy = `HTTP/1.1 200
Content-Type: application/json; charset=UTF-8

{
"version": 10,
"kind": "storage#policy",
"resourceId": "string",
"bindings": [
{
"role": "string",
"members": [
"string"
]
"condition": {
"title": "string",
"description": "string",
"expression": "2023-08-13 16:08:44+02:00"
}
}
],
"etag": "string"
}`

listPoliciesResultResponse = `HTTP/1.1 200
Content-Type: application/json; charset=UTF-8

Expand Down
39 changes: 34 additions & 5 deletions pkg/processor/compliance_processor_factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,18 @@ func (c *backupWithSingleWriterCheck) Check(ctx context.Context, request request
it := policiesClient.ListPolicies(ctx, &iampb.ListPoliciesRequest{
Parent: fmt.Sprintf("policies/%s/denypolicies", attachmentPoint),
})

gcsClient, err := gcs.NewCloudStorageClient(ctx, c.tokenSourceProvider, targetProject)
if err != nil {
glog.Errorf("could not create new GCS client: %s", targetProject, err)
return requestobjects.ComplianceCheck{}, err
}
defer gcsClient.Close(ctx)
project, err := gcsClient.GetProject(ctx, targetProject)
if err != nil {
glog.Errorf("could not get project %s info: %s", targetProject, err)
return requestobjects.ComplianceCheck{}, err
}
stsSinkAccount := fmt.Sprintf(sinkSTSAccountScheme, strings.ReplaceAll(project.Name, "projects/", ""))
for {
policy, err := it.Next()
if errors.Is(err, iterator.Done) {
Expand All @@ -331,10 +342,18 @@ func (c *backupWithSingleWriterCheck) Check(ctx context.Context, request request
glog.Errorf("could not get next policy: %s", err)
break
}
// list policies for the target project comes without rules
fullPolicy, err := policiesClient.GetPolicy(ctx, &iampb.GetPolicyRequest{
Name: policy.Name,
})
if err != nil {
glog.Errorf("could not get full 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 {
for _, rule := range fullPolicy.Rules {
deniedPermissions := rule.GetDenyRule().GetDeniedPermissions()
deniedPrincipals := rule.GetDenyRule().GetDeniedPrincipals()
exceptionPrincipals := rule.GetDenyRule().GetExceptionPrincipals()
Expand All @@ -347,7 +366,7 @@ func (c *backupWithSingleWriterCheck) Check(ctx context.Context, request request
continue
}

if !containsOnlyBackupServiceAccountAsException(targetPrincipal, exceptionPrincipals) {
if !containsOnlyBackupServiceAccountsAsException(stsSinkAccount, targetPrincipal, exceptionPrincipals) {
continue
}

Expand Down Expand Up @@ -384,8 +403,18 @@ const (
allPrincipals = "principalSet://goog/public:all"
)

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 containsOnlyBackupServiceAccountsAsException(stsServiceAccount, targetPrincipal string, principals []string) bool {
// sts & backup service accounts can write
allowedPrincipals := []string{
fmt.Sprintf("principal://iam.googleapis.com/projects/-/serviceAccounts/%s", targetPrincipal),
fmt.Sprintf("principal://iam.googleapis.com/projects/-/serviceAccounts/%s", stsServiceAccount),
}
for _, principal := range principals {
if !contains(allowedPrincipals, principal) {
return false
}
}
return len(principals) == len(allowedPrincipals)
}

func containsAllPrincipals(principals []string) bool {
Expand Down
29 changes: 27 additions & 2 deletions pkg/processor/creating_processor_factory.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package processor

import (
"cloud.google.com/go/iam"
"context"
"fmt"
"strings"
Expand All @@ -20,6 +21,8 @@ import (
"go.opencensus.io/trace"
)

const sinkSTSAccountScheme = "project-%[email protected]"

type CreatingProcessorFactory interface {
CreateProcessor(ctxIn context.Context) (Operation[requestobjects.CreateRequest, requestobjects.BackupResponse], error)
}
Expand Down Expand Up @@ -420,8 +423,30 @@ func prepareSink(ctxIn context.Context, cloudStorageClient gcs.CloudStorageClien
}

err = cloudStorageClient.CreateBucket(ctx, backup.TargetProject, backup.Sink, backup.Region, backup.DualRegion, backup.StorageClass, lifetimeInDays, backup.ArchiveTTM)
if err == nil {
return cloudStorageClient.CreateObject(ctx, backup.Sink, fmt.Sprintf("%s/THIS_TRASHCAN_CONTAINS_DELETED_OBJECTS_FROM_SOURCE", backup.GetTrashcanPath()), "")
if err != nil {
return err
}
err = cloudStorageClient.CreateObject(ctx, backup.Sink, fmt.Sprintf("%s/THIS_TRASHCAN_CONTAINS_DELETED_OBJECTS_FROM_SOURCE", backup.GetTrashcanPath()), "")
if err != nil {
return err
}
if backup.Type != repository.CloudStorage {
return nil
}
project, err := cloudStorageClient.GetProject(ctx, backup.TargetProject)
if err != nil {
return err
}
projectNumber := strings.ReplaceAll(project.Name, "projects/", "")
bucketPolicy := &iam.Policy{}
// Storage Transfer Service needs to write to and read from the sink bucket
// based on https://cloud.google.com/storage-transfer/docs/sink-cloud-storage#required_permissions
storageTransferGSABinding := "serviceAccount:" + fmt.Sprintf(sinkSTSAccountScheme, projectNumber)
bucketPolicy.Add(storageTransferGSABinding, "roles/storage.legacyBucketWriter")
bucketPolicy.Add(storageTransferGSABinding, "roles/storage.legacyBucketReader")
err = cloudStorageClient.SetBucketIAMPolicy(ctx, backup.Sink, bucketPolicy)
if err != nil {
return err
}
}

Expand Down
10 changes: 10 additions & 0 deletions pkg/processor/schedule_processor_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package processor

import (
"cloud.google.com/go/iam"
"cloud.google.com/go/resourcemanager/apiv3/resourcemanagerpb"
"context"
"regexp"
"testing"
Expand Down Expand Up @@ -443,6 +445,14 @@ type stubGcsClient struct {
fDeleteObjectsErr error
}

func (g *stubGcsClient) GetProject(ctxIn context.Context, projectID string) (*resourcemanagerpb.Project, error) {
panic("implement me")
}

func (g *stubGcsClient) SetBucketIAMPolicy(ctxIn context.Context, bucket string, policy *iam.Policy) error {
panic("implement me")
}

func (g *stubGcsClient) Close(context.Context) {
panic("implement me")
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/processor/updating_processor_factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ func (c updatingProcessor) Process(ctxIn context.Context, args *Argument[request
// handle status change
if request.Status != "" && !backup.Status.EqualTo(request.Status) {
if !isBackupStatusTransitionValid(backup.Status, repository.BackupStatus(request.Status)) {
return requestobjects.UpdateResponse{}, fmt.Errorf("backup status update not allowed from %s to %s", request.BackupID, request.Status)
return requestobjects.UpdateResponse{}, fmt.Errorf("backup status update not allowed from %s to %s", backup.Status, request.Status)
}
// make a shortcut from NotStarted -> ToDelete to NotStarted -> BackupDeleted
if repository.NotStarted == backup.Status && repository.ToDelete.EqualTo(request.Status) {
Expand Down
20 changes: 15 additions & 5 deletions pkg/repository/backup_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,19 +212,29 @@ func (d *defaultBackupRepository) UpdateBackup(ctxIn context.Context, fields Upd
}

columns := []string{
"snapshot_lifetime_in_days",
"bigquery_table",
"bigquery_excluded_tables",
"cloudstorage_include_path",
"cloudstorage_exclude_path",
"audit_updated_timestamp",
"audit_deleted_timestamp",
"mirror_lifetime_in_days",
"archive_ttm",
"recovery_point_objective",
"recovery_time_objective",
}

if fields.RecoveryPointObjective > 0 {
columns = append(columns, "recovery_point_objective")
}
if fields.RecoveryTimeObjective > 0 {
columns = append(columns, "recovery_time_objective")
}
if fields.ArchiveTTM > 0 {
columns = append(columns, "archive_ttm")
}
if fields.MirrorTTL > 0 {
columns = append(columns, "mirror_lifetime_in_days")
}
if fields.SnapshotTTL > 0 {
columns = append(columns, "snapshot_lifetime_in_days")
}
if fields.Status != "" {
columns = append(columns, "status")
}
Expand Down
Loading
Loading