diff --git a/lib/awsorgs/organization.go b/lib/awsorgs/organization.go index ee464bd..e0b80a6 100644 --- a/lib/awsorgs/organization.go +++ b/lib/awsorgs/organization.go @@ -434,6 +434,61 @@ func (c Client) ListAccountsForParent(parentID string) ([]*organizations.Account return accounts, oops.Wrapf(err, "organizations.ListAccountsForParent") } +func (c Client) DelegateAdmin(ctx context.Context, acctID, servicePrincipal string) error { + if _, err := c.organizationClient.EnableAWSServiceAccessWithContext(ctx, &organizations.EnableAWSServiceAccessInput{ + ServicePrincipal: &servicePrincipal, + }); err != nil { + return oops.Wrapf(err, "organizations.EnableAWSServiceAccess service %s", servicePrincipal) + } + + _, err := c.organizationClient.RegisterDelegatedAdministratorWithContext(ctx, &organizations.RegisterDelegatedAdministratorInput{ + AccountId: &acctID, + ServicePrincipal: &servicePrincipal, + }) + if err != nil { + return oops.Wrapf(err, "organizations.RegisterDelegatedAdministrator service %s", servicePrincipal) + } + + return nil +} + +// FetchDelegatedAdminPrincipals returns a list of accounts that have delegated +// admin permissions and the service principals with a key of account ID and +// value with a slice of service principals that are delegated to the account key. +func (c Client) FetchDelegatedAdminPrincipals(ctx context.Context) (map[string][]string, error) { + var delegatedAccounts []string + err := c.organizationClient.ListDelegatedAdministratorsPagesWithContext(ctx, &organizations.ListDelegatedAdministratorsInput{}, + func(page *organizations.ListDelegatedAdministratorsOutput, lastPage bool) bool { + for _, acct := range page.DelegatedAdministrators { + delegatedAccounts = append(delegatedAccounts, *acct.Id) + } + return !lastPage + }) + if err != nil { + return nil, oops.Wrapf(err, "organizations.ListDelegatedAdministrators") + } + + // Now we need to see what services are enabled for each account + resp := make(map[string][]string) + for _, acct := range delegatedAccounts { + var servicePrincipals []string + err := c.organizationClient.ListDelegatedServicesForAccountPagesWithContext(ctx, &organizations.ListDelegatedServicesForAccountInput{ + AccountId: &acct, + }, func(page *organizations.ListDelegatedServicesForAccountOutput, lastPage bool) bool { + for _, service := range page.DelegatedServices { + servicePrincipals = append(servicePrincipals, *service.ServicePrincipal) + } + return !lastPage + }) + if err != nil { + return nil, oops.Wrapf(err, "organizations.ListDelegatedServicesForAccount acctID: %s", acct) + } + resp[acct] = servicePrincipals + } + + return resp, nil +} + func (c Client) FetchOUAndDescendents(ctx context.Context, ouID, mgmtAccountID string) (resource.OrganizationUnit, error) { var ou resource.OrganizationUnit diff --git a/mintlifydocs/config/organization.mdx b/mintlifydocs/config/organization.mdx index d17bcfd..acb08db 100644 --- a/mintlifydocs/config/organization.mdx +++ b/mintlifydocs/config/organization.mdx @@ -49,6 +49,7 @@ Accounts: # If deleting an account you need to pass in --allow-account-delete to telophasecli as a confirmation of the deletion. Tags: # (Optional) Telophase label for this account. Tags translate to AWS tags with a `=` as the key value delimiter. For example, `telophase:env=prod` Stacks: # (Optional) Terraform, Cloudformation and CDK stacks to apply to all accounts in this Organization Unit. + DelegatedAdministratorServices: # (Optional) List of delegated service principals for the current account (e.g. config.amazonaws.com) ``` ## Example diff --git a/resource/account.go b/resource/account.go index 13eeacd..c1c3abd 100644 --- a/resource/account.go +++ b/resource/account.go @@ -22,9 +22,10 @@ type Account struct { ServiceControlPolicies []Stack `yaml:"ServiceControlPolicies,omitempty"` ManagementAccount bool `yaml:"-"` - Delete bool `yaml:"Delete"` - DelegatedAdministrator bool `yaml:"DelegatedAdministrator,omitempty"` - Parent *OrganizationUnit `yaml:"-"` + Delete bool `yaml:"Delete"` + DelegatedAdministrator bool `yaml:"DelegatedAdministrator,omitempty"` + DelegatedAdministratorServices []string `yaml:"DelegatedAdministratorServices,omitempty"` + Parent *OrganizationUnit `yaml:"-"` Status string `yaml:"-,omitempty"` } diff --git a/resourceoperation/account.go b/resourceoperation/account.go index b972094..6af1692 100644 --- a/resourceoperation/account.go +++ b/resourceoperation/account.go @@ -26,6 +26,8 @@ type accountOperation struct { OrgClient *awsorgs.Client TagsDiff *TagsDiff AllowDelete bool + + DelegateAdminPrincipal string } func NewAccountOperation( @@ -54,6 +56,10 @@ func (ao *accountOperation) SetAllowDelete(allowDelete bool) { ao.AllowDelete = allowDelete } +func (ao *accountOperation) SetDelegateAdminPrincipal(principal string) { + ao.DelegateAdminPrincipal = principal +} + func CollectAccountOps( ctx context.Context, consoleUI runner.ConsoleUI, @@ -147,6 +153,11 @@ func (ao *accountOperation) Call(ctx context.Context) error { if err != nil { return oops.Wrapf(err, "CloseAccounts") } + } else if ao.Operation == DelegateAdmin { + err := ao.OrgClient.DelegateAdmin(ctx, ao.Account.AccountID, ao.DelegateAdminPrincipal) + if err != nil { + return oops.Wrapf(err, "DelegateAdmin principal: %s", ao.DelegateAdminPrincipal) + } } for _, op := range ao.DependentOperations { @@ -221,6 +232,13 @@ Tags: ` printColor = "red" } } + } else if ao.Operation == DelegateAdmin { + templated = "\n" + `(Delegate Service) +ID: {{ .Account.AccountID }} +Name: {{ .Account.AccountName }} ++ ServicePrincipal: {{ .DelegateAdminPrincipal }} +` + printColor = "green" } tpl, err := template.New("operation").Funcs(template.FuncMap{ diff --git a/resourceoperation/interface.go b/resourceoperation/interface.go index 1b53a4d..a8feccf 100644 --- a/resourceoperation/interface.go +++ b/resourceoperation/interface.go @@ -6,11 +6,12 @@ import ( const ( // Accounts - UpdateParent = 1 - Create = 2 - Update = 3 - UpdateTags = 6 - Delete = 7 + UpdateParent = 1 + Create = 2 + Update = 3 + UpdateTags = 6 + Delete = 7 + DelegateAdmin = 8 // IaC Diff = 4 diff --git a/resourceoperation/organization_unit.go b/resourceoperation/organization_unit.go index a845b0d..2e95b02 100644 --- a/resourceoperation/organization_unit.go +++ b/resourceoperation/organization_unit.go @@ -77,6 +77,11 @@ func CollectOrganizationUnitOps( return []ResourceOperation{} } + delegatedAdmins, err := orgClient.FetchDelegatedAdminPrincipals(ctx) + if err != nil { + consoleUI.Print(fmt.Sprintf("Failed to fetch delegated admins, continuing anyway, error: %v", err), *mgmtAcct) + } + providerOUs := providerRootOU.AllDescendentOUs() for _, parsedOU := range rootOU.AllDescendentOUs() { var found bool @@ -251,6 +256,25 @@ func CollectOrganizationUnitOps( operations = append(operations, op) } + if len(parsedAcct.DelegatedAdministratorServices) > 0 && delegatedAdmins != nil { + for _, delegatedAdminService := range parsedAcct.DelegatedAdministratorServices { + if services := delegatedAdmins[parsedAcct.AccountID]; !oneOf(delegatedAdminService, services) { + op := NewAccountOperation( + orgClient, + consoleUI, + parsedAcct, + mgmtAcct, + DelegateAdmin, + nil, + nil, + nil, + ) + op.SetDelegateAdminPrincipal(delegatedAdminService) + operations = append(operations, op) + } + } + } + break } }