Skip to content

Commit

Permalink
resourceop account: allow delegating services to accounts
Browse files Browse the repository at this point in the history
This allows someone to delegate different service principals to accounts
managed in telophase. One example is:

```
    OrganizationUnits:
      - Name: Security
        Accounts:
          - Email: example+audit@example.com
            AccountName: Audit
            DelegatedAdministratorServices:
              - "config.amazonaws.com"
              - "config-multiaccountsetup.amazonaws.com"
```

This will end up calling `register-delegated-admin` with the listed
service principals on the account defined.
  • Loading branch information
dschofie committed Jun 4, 2024
1 parent 0e1d824 commit 187d507
Show file tree
Hide file tree
Showing 6 changed files with 108 additions and 8 deletions.
55 changes: 55 additions & 0 deletions lib/awsorgs/organization.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions mintlifydocs/config/organization.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 4 additions & 3 deletions resource/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
Expand Down
18 changes: 18 additions & 0 deletions resourceoperation/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ type accountOperation struct {
OrgClient *awsorgs.Client
TagsDiff *TagsDiff
AllowDelete bool

DelegateAdminPrincipal string
}

func NewAccountOperation(
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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{
Expand Down
11 changes: 6 additions & 5 deletions resourceoperation/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 24 additions & 0 deletions resourceoperation/organization_unit.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
}
Expand Down

0 comments on commit 187d507

Please sign in to comment.