Repository: gruntwork-io/cloud-nuke
Pull request #639 (2024-02-15 21:56:38)

Check for ownership when nuking transit gateway and show apprporiate error
<!-- Prepend '[WIP]' to the title if this PR is still a work-in-progress. Remove it when it is ready for review! -->

## Description

Fixes https://github.com/gruntwork-io/cloud-nuke/issues/361.

![image](https://github.com/gruntwork-io/cloud-nuke/assets/96548424/9b0aea3a-4e82-4e04-9203-d188c53fe3f7)
![image](https://github.com/gruntwork-io/cloud-nuke/assets/96548424/7cdde172-7142-4ede-bf56-b217235ad772)


<!-- Description of the changes introduced by this PR. -->

## TODOs

Read the [Gruntwork contribution guidelines](https://gruntwork.notion.site/Gruntwork-Coding-Methodology-02fdcd6e4b004e818553684760bf691e).

- [ ] Update the docs.
- [ ] Run the relevant tests successfully, including pre-commit checks.
- [ ] Ensure any 3rd party code adheres with our [license policy](https://www.notion.so/gruntwork/Gruntwork-licenses-and-open-source-usage-policy-f7dece1f780341c7b69c1763f22b1378) or delete this line if its not applicable.
- [ ] Include release notes. If this PR is backward incompatible, include a migration guide.
- [ ] Attention Grunts - if this PR adds support for a new resource, ensure the `nuke_sandbox` and `nuke_phxdevops` jobs in `.circleci/config.yml` have been updated with appropriate exclusions (either directly in the job or via the `.circleci/nuke_config.yml` file) to prevent nuking IAM roles, groups, resources, etc that are important for the test accounts.


## Release Notes (draft)

<!-- One-line description of the PR that can be included in the final release notes. -->
Added / Removed / Updated [X].

### Migration Guide

<!-- Important: If you made any backward incompatible changes, then you must write a migration guide! -->




Patch:
diff --git a/README.md b/README.md
index f67d91d..bff9208 100644
--- a/README.md
+++ b/README.md
@@ -747,6 +747,11 @@ job for this repo has been configured to:
 
 See `.circleci/config.yml` for details.
 
+## Nukable error statuses
+You'll encounter any of the following statuses when attempting to nuke resources, and here's what each status means:
+- `error:INSUFFICIENT_PERMISSION` : You don't have enough permission to nuke the resource.
+- `error:DIFFERENT_OWNER` : You are attempting to nuke a resource for which you are not the owner.
+
 ## License
 
 This code is released under the MIT License. See [LICENSE.txt](/LICENSE.txt).
diff --git a/aws/resources/base_resource.go b/aws/resources/base_resource.go
index 5c71408..a028abb 100644
--- a/aws/resources/base_resource.go
+++ b/aws/resources/base_resource.go
@@ -3,18 +3,24 @@ package resources
 import (
 	"context"
 	"errors"
+	"fmt"
 
 	"github.com/aws/aws-sdk-go/aws/session"
 	"github.com/gruntwork-io/cloud-nuke/config"
+	"github.com/gruntwork-io/cloud-nuke/util"
 )
 
-// BaseAwsResource This BaseAwsResource struct and its associated methods to serve as a placeholder or template for a resource
-// that is not yet fully implemented within a system or framework.
-// Its purpose is to provide a skeleton structure that adheres to a specific interface or contract expected by the
-// system without containing the actual implementation details.
-type BaseAwsResource struct{}
+// BaseAwsResource struct and its associated methods to serve as a placeholder or template for a resource that is not
+// yet fully implemented within a system or framework. Its purpose is to provide a skeleton structure that adheres to a
+// specific interface or contract expected by the system without containing the actual implementation details.
+type BaseAwsResource struct {
+	// A key-value of identifiers and nukable status
+	Nukables map[string]error
+}
 
-func (umpl *BaseAwsResource) Init(_ *session.Session) {}
+func (umpl *BaseAwsResource) Init(_ *session.Session) {
+	umpl.Nukables = make(map[string]error)
+}
 func (umpl *BaseAwsResource) ResourceName() string {
 	return "not implemented: ResourceName"
 }
@@ -30,6 +36,39 @@ func (umpl *BaseAwsResource) Nuke(_ []string) error {
 func (umpl *BaseAwsResource) GetAndSetIdentifiers(_ context.Context, _ config.Config) ([]string, error) {
 	return nil, errors.New("not implemented: GetAndSetIdentifiers")
 }
-func (umpl *BaseAwsResource) IsNukable(_ string) (bool, error) {
-	return false, errors.New("not implemented yet.")
+
+func (umpl *BaseAwsResource) GetNukableStatus(identifier string) (error, bool) {
+	val, ok := umpl.Nukables[identifier]
+	return val, ok
+}
+
+func (umpl *BaseAwsResource) SetNukableStatus(identifier string, err error) {
+	umpl.Nukables[identifier] = err
+}
+
+// VerifyNukablePermissions performs nukable permission verification for each ID. For each ID, the function is
+// executed, and the result (error or success) is recorded using the SetNukableStatus method, indicating whether
+// the specified action is nukable
+func (umpl *BaseAwsResource) VerifyNukablePermissions(ids []*string, nukableCheckfn func(id *string) error) {
+	for _, id := range ids {
+		// skip if the id is already exists
+		if _, ok := umpl.GetNukableStatus(*id); ok {
+			continue
+		}
+		err := nukableCheckfn(id)
+		umpl.SetNukableStatus(*id, util.TransformAWSError(err))
+	}
+}
+
+func (umpl *BaseAwsResource) IsNukable(identifier string) (bool, error) {
+	err, ok := umpl.Nukables[identifier]
+	if !ok {
+		return false, fmt.Errorf("-")
+	}
+
+	if err != nil {
+		return false, err
+	}
+
+	return true, nil
 }
diff --git a/aws/resources/transit_gateway.go b/aws/resources/transit_gateway.go
index 336ad03..b610f10 100644
--- a/aws/resources/transit_gateway.go
+++ b/aws/resources/transit_gateway.go
@@ -2,12 +2,12 @@ package resources
 
 import (
 	"context"
-	cerrors "errors"
 	"time"
 
 	"github.com/aws/aws-sdk-go/aws"
 	awsgo "github.com/aws/aws-sdk-go/aws"
 	"github.com/aws/aws-sdk-go/service/ec2"
+	goerror "github.com/go-errors/errors"
 	"github.com/gruntwork-io/cloud-nuke/config"
 	"github.com/gruntwork-io/cloud-nuke/logging"
 	"github.com/gruntwork-io/cloud-nuke/report"
@@ -17,25 +17,11 @@ import (
 	commonTelemetry "github.com/gruntwork-io/go-commons/telemetry"
 )
 
-/*
-NOTE on the Apporach used:-Using the `dry run` approach on verifying the nuking permission in case of a scoped IAM role.
-IAM:simulateCustomPolicy : could also be used but the IAM role itself needs permission for simulateCustomPolicy method
-else this would not get the desired result. Also in case of multiple t-gateway, if only some has permssion to be nuked,
-the t-gateway resource ids needs to be passed individually inside the IAM:simulateCustomPolicy to get the desired result,
-else all would result in `Implicit-deny` as response- this might increase the time complexity.Using dry run to avoid this.
-*/
-func (tgw *TransitGateways) VerifyNukablePermissions(ctx context.Context, ids []*string) {
-	// check permissions without actually performing the nuke operation
-	for _, id := range ids {
-		// dry run set as true , checks permission without actualy making the request
-		params := &ec2.DeleteTransitGatewayInput{
-			TransitGatewayId: id,
-			DryRun:           aws.Bool(true),
-		}
-		_, err := tgw.Client.DeleteTransitGateway(params)
-		tgw.Nukable[*id] = !util.IsAwsUnauthorizedError(err)
-	}
-}
+// [Note 1] :  NOTE on the Apporach used:-Using the `dry run` approach on verifying the nuking permission in case of a scoped IAM role.
+// IAM:simulateCustomPolicy : could also be used but the IAM role itself needs permission for simulateCustomPolicy method
+//else this would not get the desired result. Also in case of multiple t-gateway, if only some has permssion to be nuked,
+// the t-gateway resource ids needs to be passed individually inside the IAM:simulateCustomPolicy to get the desired result,
+// else all would result in `Implicit-deny` as response- this might increase the time complexity.Using dry run to avoid this.
 
 // Returns a formatted string of TransitGateway IDs
 func (tgw *TransitGateways) getAll(c context.Context, configObj config.Config) ([]*string, error) {
@@ -46,16 +32,32 @@ func (tgw *TransitGateways) getAll(c context.Context, configObj config.Config) (
 		return nil, errors.WithStackTrace(err)
 	}
 
+	currentOwner := c.Value(util.AccountIdKey)
 	var ids []*string
 	for _, transitGateway := range result.TransitGateways {
 		if configObj.TransitGateway.ShouldInclude(config.ResourceValue{Time: transitGateway.CreationTime}) &&
 			awsgo.StringValue(transitGateway.State) != "deleted" && awsgo.StringValue(transitGateway.State) != "deleting" {
 			ids = append(ids, transitGateway.TransitGatewayId)
 		}
+
+		if currentOwner != nil && transitGateway.OwnerId != nil && currentOwner != awsgo.StringValue(transitGateway.OwnerId) {
+			tgw.SetNukableStatus(*transitGateway.TransitGatewayId, util.ErrDifferentOwner)
+			continue
+		}
 	}
 
 	// Check and verfiy the list of allowed nuke actions
-	tgw.VerifyNukablePermissions(c, ids)
+	// VerifyNukablePermissions is used to iterate over a list of Transit Gateway IDs (ids) and execute a provided function (func(id *string) error).
+	// The function, attempts to delete a Transit Gateway with the specified ID in a dry-run mode (checking permissions without actually performing the delete operation). The result of this operation (error or success) is then captured.
+	// See more at [Note 1]
+	tgw.VerifyNukablePermissions(ids, func(id *string) error {
+		params := &ec2.DeleteTransitGatewayInput{
+			TransitGatewayId: id,
+			DryRun:           aws.Bool(true), // dry run set as true , checks permission without actualy making the request
+		}
+		_, err := tgw.Client.DeleteTransitGateway(params)
+		return err
+	})
 
 	return ids, nil
 }
@@ -73,9 +75,9 @@ func (tgw *TransitGateways) nukeAll(ids []*string) error {
 
 	for _, id := range ids {
 		//check the id has the permission to nuke, if not. continue the execution
-		if nukable, err := tgw.IsNukable(*id); !nukable && err == nil {
+		if nukable, err := tgw.IsNukable(*id); !nukable {
 			//not adding the report on final result hence not adding a record entry here
-			logging.Debugf("[Skipping] %s nuke while you didn't have the permission", *id)
+			logging.Debugf("[Skipping] %s nuke because %v", *id, err)
 			continue
 		}
 
@@ -219,7 +221,7 @@ func (tgw *TransitGatewaysVpcAttachment) nukeAll(ids []*string) error {
 		}
 	}
 
-	if waiterr := waitForTransitGatewayAttachmentToBeDeleted(*tgw); waiterr != nil {
+	if waiterr := waitForTransitGatewayAttachementToBeDeleted(*tgw); waiterr != nil {
 		return errors.WithStackTrace(waiterr)
 	}
 	logging.Debugf(("[OK] %d Transit Gateway Vpc Attachment(s) deleted in %s"), len(deletedIds), tgw.Region)
@@ -267,7 +269,7 @@ func (tgpa *TransitGatewayPeeringAttachment) nukeAll(ids []*string) error {
 	return nil
 }
 
-func waitForTransitGatewayAttachmentToBeDeleted(tgw TransitGatewaysVpcAttachment) error {
+func waitForTransitGatewayAttachementToBeDeleted(tgw TransitGatewaysVpcAttachment) error {
 	for i := 0; i < 30; i++ {
 		gateways, err := tgw.Client.DescribeTransitGatewayVpcAttachments(
 			&ec2.DescribeTransitGatewayVpcAttachmentsInput{
@@ -290,5 +292,5 @@ func waitForTransitGatewayAttachmentToBeDeleted(tgw TransitGatewaysVpcAttachment
 		time.Sleep(10 * time.Second)
 	}
 
-	return cerrors.New("timed out waiting for transit gateway attachments to be successfully deleted")
+	return goerror.New("timed out waiting for transit gateway attahcments to be successfully deleted")
 }
diff --git a/aws/resources/transit_gateway_types.go b/aws/resources/transit_gateway_types.go
index d08503f..7954db3 100644
--- a/aws/resources/transit_gateway_types.go
+++ b/aws/resources/transit_gateway_types.go
@@ -151,13 +151,14 @@ type TransitGateways struct {
 	Client ec2iface.EC2API
 	Region string
 	Ids    []string
-	// A key-value of identifiers and nukable status
-	Nukable map[string]bool
 }
 
 func (tgw *TransitGateways) Init(session *session.Session) {
+	// to initialize base resource
+	// NOTE : This is madatory to initialize the nukables map
+	tgw.BaseAwsResource.Init(session)
 	tgw.Client = ec2.New(session)
-	tgw.Nukable = map[string]bool{}
+
 }
 
 // ResourceName - the simple name of the aws resource
@@ -193,12 +194,3 @@ func (tgw *TransitGateways) Nuke(identifiers []string) error {
 
 	return nil
 }
-
-// IsNukable - Checks whether the given identifier is authorized to nuke
-func (tgw *TransitGateways) IsNukable(identifier string) (bool, error) {
-	if status, ok := tgw.Nukable[identifier]; ok && status {
-		return true, nil
-	}
-
-	return false, nil
-}
diff --git a/ui/ui.go b/ui/ui.go
index b2c3eb9..b1db2a3 100644
--- a/ui/ui.go
+++ b/ui/ui.go
@@ -138,10 +138,12 @@ func RenderResourcesAsTable(account *aws.AwsAccountResources) error {
 	for region, resourcesInRegion := range account.Resources {
 		for _, foundResources := range resourcesInRegion.Resources {
 			for _, identifier := range (*foundResources).ResourceIdentifiers() {
-				isnukable := "-"
-				if marked, err := (*foundResources).IsNukable(identifier); err == nil {
-					isnukable = fmt.Sprintf("%v", marked)
+				isnukable := SuccessEmoji
+				_, err := (*foundResources).IsNukable(identifier)
+				if err != nil {
+					isnukable = err.Error()
 				}
+
 				tableData = append(tableData, []string{(*foundResources).ResourceName(), region, identifier, isnukable})
 			}
 		}
diff --git a/util/error.go b/util/error.go
index 3be79fb..f9791c5 100644
--- a/util/error.go
+++ b/util/error.go
@@ -1,17 +1,23 @@
 package util
 
 import (
+	"errors"
 	"github.com/aws/aws-sdk-go/aws/awserr"
 )
 
+var ErrInSufficientPermission = errors.New("error:INSUFFICIENT_PERMISSION")
+var ErrDifferentOwner = errors.New("error:DIFFERENT_OWNER")
+
 const AWsUnauthorizedError string = "UnauthorizedOperation"
 
-// IsAwsUnauthorizedError : checks whether the aws returned error is AWsUnauthorizedError
-// For any unauthorised error we can use the same code as AWS returns the same code in this scenario
+// TransformAWSError
+// this function is used to handle AWS errors and mapping them to a custom error message
+// This could be part of a larger error-handling strategy that interacts with AWS services,
+// providing a more human-readable error message for certain conditions
 // ref : https://docs.aws.amazon.com/AWSEC2/latest/APIReference/errors-overview.html
-func IsAwsUnauthorizedError(err error) bool {
+func TransformAWSError(err error) error {
 	if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == AWsUnauthorizedError {
-		return true
+		return ErrInSufficientPermission
 	}
-	return false
+	return nil
 }


Test patch:
diff --git a/aws/resources/transit_gateway_test.go b/aws/resources/transit_gateway_test.go
index 7760216..0be916a 100644
--- a/aws/resources/transit_gateway_test.go
+++ b/aws/resources/transit_gateway_test.go
@@ -87,7 +87,6 @@ func TestTransitGateways_GetAll(t *testing.T) {
 	gatewayId1 := "gateway1"
 	gatewayId2 := "gateway2"
 	tgw := TransitGateways{
-		Nukable: make(map[string]bool),
 		Client: mockedTransitGateway{
 			DescribeTransitGatewaysOutput: ec2.DescribeTransitGatewaysOutput{
 				TransitGateways: []*ec2.TransitGateway{
@@ -105,6 +104,8 @@ func TestTransitGateways_GetAll(t *testing.T) {
 			},
 		},
 	}
+	tgw.BaseAwsResource.Init(nil)
+
 	tests := map[string]struct {
 		configObj config.ResourceType
 		expected  []string