Skip to content

Commit

Permalink
Support deleting cloud accounts (#2060)
Browse files Browse the repository at this point in the history
Support deleting cloud accounts in posture

#2044
  • Loading branch information
gnmahanth authored Apr 10, 2024
1 parent 9ae92b0 commit 887e90e
Show file tree
Hide file tree
Showing 9 changed files with 338 additions and 0 deletions.
4 changes: 4 additions & 0 deletions deepfence_server/apiDocs/operation.go
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,10 @@ func (d *OpenAPIDocs) AddCloudNodeOperations() {
"Register Cloud Node Account", "Register Cloud Node Account and return any pending compliance scans from console",
http.StatusOK, []string{tagCloudNodes}, bearerToken, new(CloudNodeAccountRegisterReq), new(CloudNodeAccountRegisterResp))

d.AddOperation("deleteCloudNodeAccount", http.MethodDelete, "/deepfence/cloud-node/account",
"Delete Cloud Node Account", "Delete Cloud Node Account and related resources",
http.StatusAccepted, []string{tagCloudNodes}, bearerToken, new(CloudAccountDeleteReq), nil)

d.AddOperation("listCloudNodeAccount", http.MethodPost, "/deepfence/cloud-node/list/accounts",
"List Cloud Node Accounts", "List Cloud Node Accounts registered with the console",
http.StatusOK, []string{tagCloudNodes}, bearerToken, new(CloudNodeAccountsListReq), new(CloudNodeAccountsListResp))
Expand Down
1 change: 1 addition & 0 deletions deepfence_server/auth/policy.csv
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ p, admin, cloud-node, register
p, admin, cloud-node, write
p, admin, cloud-node, read
p, admin, cloud-node, update
p, admin, cloud-node, delete
p, standard-user, cloud-node, register
p, standard-user, cloud-node, write
p, standard-user, cloud-node, read
Expand Down
40 changes: 40 additions & 0 deletions deepfence_server/handler/cloud_node.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package handler

import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
Expand Down Expand Up @@ -324,3 +325,42 @@ func (h *Handler) CachePostureProviders(ctx context.Context) error {
}
return nil
}

func (h *Handler) DeleteCloudAccountHandler(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
var req model.CloudAccountDeleteReq
err := httpext.DecodeJSON(r, httpext.NoQueryParams, MaxPostRequestSize, &req)
if err != nil {
log.Error().Msgf("%v", err)
h.respondError(&BadDecoding{err}, w)
return
}

log.Info().Msgf("delete cloud accounts request: %v", req)

if len(req.NodeIDs) > 0 {
worker, err := directory.Worker(r.Context())
if err != nil {
log.Error().Msgf("%v", err)
h.respondError(&InternalServerError{err}, w)
return
}

data, err := json.Marshal(req)
if err != nil {
log.Error().Err(err).Msg("failed to marshal cloud account delete request")
h.respondError(&InternalServerError{err}, w)
return
}

if err := worker.Enqueue(utils.DeleteCloudAccounts, data, utils.CritialTaskOpts()...); err != nil {
log.Error().Err(err).Msg("failed enqueue task delete cloud accounts")
h.respondError(&InternalServerError{err}, w)
return
}
}

h.AuditUserActivity(r, EventComplianceScan, ActionDelete, req, true)

w.WriteHeader(http.StatusAccepted)
}
4 changes: 4 additions & 0 deletions deepfence_server/model/cloud_node.go
Original file line number Diff line number Diff line change
Expand Up @@ -622,3 +622,7 @@ func (c *CloudAccountRefreshReq) GetCloudAccountRefresh(ctx context.Context) ([]
}
return updatedNodeIDs, tx.Commit(ctx)
}

type CloudAccountDeleteReq struct {
NodeIDs []string `json:"node_ids" validate:"required,gt=0" required:"true"`
}
1 change: 1 addition & 0 deletions deepfence_server/router/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,7 @@ func SetupRoutes(r *chi.Mux, serverPort string, serveOpenapiDocs bool, ingestC c

r.Route("/cloud-node", func(r chi.Router) {
r.Post("/account", dfHandler.AuthHandler(ResourceCloudNode, PermissionRegister, dfHandler.RegisterCloudNodeAccountHandler))
r.Delete("/account", dfHandler.AuthHandler(ResourceCloudNode, PermissionDelete, dfHandler.DeleteCloudAccountHandler))
r.Post("/account/refresh", dfHandler.AuthHandler(ResourceCloudNode, PermissionWrite, dfHandler.RefreshCloudAccountHandler))
r.Post("/list/accounts", dfHandler.AuthHandler(ResourceCloudNode, PermissionRead, dfHandler.ListCloudNodeAccountHandler))
r.Get("/list/providers", dfHandler.AuthHandler(ResourceCloudNode, PermissionRead, dfHandler.ListCloudNodeProvidersHandler))
Expand Down
1 change: 1 addition & 0 deletions deepfence_utils/utils/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ const (
AutoFetchGenerativeAIIntegrations = "auto_fetch_generative_ai_integrations"
AsynqDeleteAllArchivedTasks = "asynq_delete_all_archived_tasks"
RedisRewriteAOF = "redis_rewrite_aof"
DeleteCloudAccounts = "delete_cloud_accounts"

UpdateLicenseTask = "update_license"
ReportLicenseUsageTask = "report_license_usage"
Expand Down
2 changes: 2 additions & 0 deletions deepfence_worker/ingesters/cloud_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ func ResourceToMaps(ms []ingestersUtil.CloudResource) ([]map[string]interface{},
"instance_id": newmap["node_id"],
"host_name": v.Name,
"node_id": v.Name,
"account_id": newmap["account_id"],
})
if k8sClusterName != "" {
clusters = append(clusters, map[string]interface{}{
Expand All @@ -165,6 +166,7 @@ func ResourceToMaps(ms []ingestersUtil.CloudResource) ([]map[string]interface{},
"active": true,
"cloud_provider": v.CloudProvider,
"agent_running": false,
"account_id": newmap["account_id"],
})
}
}
Expand Down
283 changes: 283 additions & 0 deletions deepfence_worker/tasks/scans/delete_cloud_accounts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
package scans

import (
"context"
"encoding/json"
"fmt"
"time"

"github.com/deepfence/ThreatMapper/deepfence_server/model"
"github.com/deepfence/ThreatMapper/deepfence_server/reporters"
reportersScan "github.com/deepfence/ThreatMapper/deepfence_server/reporters/scan"
"github.com/deepfence/ThreatMapper/deepfence_utils/directory"
"github.com/deepfence/ThreatMapper/deepfence_utils/telemetry"
"github.com/deepfence/ThreatMapper/deepfence_utils/utils"
"github.com/neo4j/neo4j-go-driver/v5/neo4j"

"github.com/deepfence/ThreatMapper/deepfence_utils/log"
"github.com/hibiken/asynq"
)

func DeleteCloudAccounts(ctx context.Context, task *asynq.Task) error {

log := log.WithCtx(ctx)

var req model.CloudAccountDeleteReq

if err := json.Unmarshal(task.Payload(), &req); err != nil {
log.Error().Err(err).Msg("failed to decode cloud account delete request")
return err
}

log.Info().Msgf("delete cloud accounts payload: %v", req)

// delete accounts
for _, accontID := range req.NodeIDs {
if err := deleteCloudAccount(ctx, accontID); err != nil {
log.Error().Err(err).Msgf("failed to delete cloud account %s", accontID)
}
}

// recompute sice we are removing everything related to the account
worker, err := directory.Worker(ctx)
if err != nil {
return err
}
return worker.Enqueue(utils.CachePostureProviders, []byte{}, utils.CritialTaskOpts()...)
}

func deleteCloudAccount(ctx context.Context, accountID string) error {

ctx, span := telemetry.NewSpan(ctx, "scans", "delete-cloud-account")
defer span.End()

log := log.WithCtx(ctx)

log.Info().Msgf("delete cloud account %s", accountID)

org, err := isOrgAccount(ctx, accountID)
if err != nil {
log.Error().Err(err).Msgf("failed to determine if org account %s", accountID)
return err
}
if org {
children, err := listOrgChildAccounts(ctx, accountID)
if err != nil {
log.Error().Err(err).Msgf("failed to list child accounts for %s", accountID)
return err
}
log.Info().Msgf("org account %s has %d children", accountID, len(children))
for _, childID := range children {
if err := deleteScans(ctx, childID); err != nil {
log.Error().Err(err).Msgf("failed to delete scans for account %s", childID)
}
if err := deleteCloudResourceAndNode(ctx, childID); err != nil {
log.Error().Err(err).Msgf("failed to delete resources for account %s", childID)
}
}
}

// just single account
if err := deleteScans(ctx, accountID); err != nil {
log.Error().Err(err).Msgf("failed to delete scans for account %s", accountID)
return err
}
return deleteCloudResourceAndNode(ctx, accountID)

}

func deleteScans(ctx context.Context, accountID string) error {

ctx, span := telemetry.NewSpan(ctx, "scans", "delete-scans")
defer span.End()

log := log.WithCtx(ctx)

// delete Cloud/ComplianceScan's related to the account first
nodeIDs := []model.NodeIdentifier{{NodeID: accountID, NodeType: "cloud_account"}}
filters := reporters.FieldsFilters{}
window := model.FetchWindow{Offset: 0, Size: 10000000}

scans, err := reportersScan.GetScansList(ctx, utils.NEO4JCloudComplianceScan, nodeIDs, filters, window)
if err != nil {
log.Error().Err(err).Msgf("failed to list scans for cloud node %s", accountID)
}

defer log.Info().Msgf("deleted %d scans for account %s", len(scans.ScansInfo), accountID)

for _, s := range scans.ScansInfo {
err := reportersScan.DeleteScan(ctx, utils.NEO4JCloudComplianceScan, s.ScanID, []string{})
if err != nil {
log.Error().Err(err).Msgf("failed to delete scan id %s", s.ScanID)
}
}

return nil
}

func deleteCloudResourceAndNode(ctx context.Context, accountID string) error {

ctx, span := telemetry.NewSpan(ctx, "scans", "delete-cloud-resources-and-node")
defer span.End()

log := log.WithCtx(ctx)

defer log.Info().Msgf("deleted cloud node and resources for account %s", accountID)

driver, err := directory.Neo4jClient(ctx)
if err != nil {
return err
}

session := driver.NewSession(ctx, neo4j.SessionConfig{AccessMode: neo4j.AccessModeWrite})
defer session.Close(ctx)

tx, err := session.BeginTransaction(ctx, neo4j.WithTxTimeout(30*time.Second))
if err != nil {
return err
}
defer tx.Close(ctx)

// delete cloud node and resources
deleteQuery := `
MATCH (n:CloudNode{node_id: $node_id})-[:OWNS]->(r:CloudResource)
DETACH DELETE n,r
`

if _, err := tx.Run(ctx, deleteQuery, map[string]any{"node_id": accountID}); err != nil {
log.Error().Err(err).Msgf("failed to delete cloud node and resources for account %s", accountID)
return err
}

// required in case where link cloud resources task has not yet run
deleteQuery1 := `MATCH (n:CloudNode{node_id: $node_id}) DETACH DELETE n`
deleteQuery2 := `MATCH (r:CloudResource{account_id: $node_id}) DETACH DELETE r`

if _, err := tx.Run(ctx, deleteQuery1, map[string]any{"node_id": accountID}); err != nil {
log.Error().Err(err).Msgf("failed to delete cloud node account %s", accountID)
return err
}

if _, err := tx.Run(ctx, deleteQuery2, map[string]any{"node_id": accountID}); err != nil {
log.Error().Err(err).Msgf("failed to delete cloud resources for account %s", accountID)
return err
}

// delete hosts discovered from cloud
deleteHostsQuery := `
MATCH (n:Node{account_id: $node_id})
WHERE n.agent_running=false
DETACH DELETE n
`

if _, err := tx.Run(ctx, deleteHostsQuery, map[string]any{"node_id": accountID}); err != nil {
log.Error().Err(err).Msgf("failed to delete hosts for account %s", accountID)
return err
}

// delete kube clusters discovered from cloud
deleteKubeClustersQuery := `
MATCH (n:KubernetesCluster{account_id: $node_id})
WHERE n.agent_running=false
DETACH DELETE n
`

if _, err := tx.Run(ctx, deleteKubeClustersQuery, map[string]any{"node_id": accountID}); err != nil {
log.Error().Err(err).Msgf("failed to delete kubernetes clusters for account %s", accountID)
return err
}

return tx.Commit(ctx)
}

func isOrgAccount(ctx context.Context, accountID string) (bool, error) {
ctx, span := telemetry.NewSpan(ctx, "scans", "check-org-account")
defer span.End()

driver, err := directory.Neo4jClient(ctx)
if err != nil {
return false, err
}

session := driver.NewSession(ctx, neo4j.SessionConfig{AccessMode: neo4j.AccessModeRead})
defer session.Close(ctx)

tx, err := session.BeginTransaction(ctx, neo4j.WithTxTimeout(30*time.Second))
if err != nil {
return false, err
}
defer tx.Close(ctx)

query := `MATCH (n:CloudNode{node_id: $node_id}) return n.cloud_provider as cloud_provider`

result, err := tx.Run(ctx, query, map[string]any{"node_id": accountID})
if err != nil {
log.Error().Err(err).Msgf("failed to delete cloud node and resources for account %s", accountID)
return false, err
}

record, err := result.Single(ctx)
if err != nil {
return false, err
}

cp, ok := record.Get("cloud_provider")
if !ok {
return false, fmt.Errorf("field not present in the result")
}

switch cp.(string) {
case model.PostureProviderAWSOrg, model.PostureProviderGCPOrg:
return true, nil
default:
return false, nil
}

}

func listOrgChildAccounts(ctx context.Context, accountID string) ([]string, error) {

ctx, span := telemetry.NewSpan(ctx, "scans", "list-org-child-accounts")
defer span.End()

childern := []string{}

driver, err := directory.Neo4jClient(ctx)
if err != nil {
return childern, err
}

session := driver.NewSession(ctx, neo4j.SessionConfig{AccessMode: neo4j.AccessModeRead})
defer session.Close(ctx)

tx, err := session.BeginTransaction(ctx, neo4j.WithTxTimeout(30*time.Second))
if err != nil {
return childern, err
}
defer tx.Close(ctx)

query := `
MATCH (n:CloudNode{node_id: $node_id})-[:IS_CHILD]->(c:CloudNode)
return c.node_id as child_id
`

result, err := tx.Run(ctx, query, map[string]any{"node_id": accountID})
if err != nil {
log.Error().Err(err).Msgf("failed to delete cloud node and resources for account %s", accountID)
return childern, err
}

records, err := result.Collect(ctx)
if err != nil {
return childern, err
}

for _, r := range records {
cid, ok := r.Get("child_id")
if ok {
childern = append(childern, cid.(string))
}
}

return childern, nil
}
Loading

0 comments on commit 887e90e

Please sign in to comment.