diff --git a/aws/plugin.go b/aws/plugin.go index aa6c0217a..c079b43a4 100644 --- a/aws/plugin.go +++ b/aws/plugin.go @@ -331,8 +331,10 @@ func Plugin(ctx context.Context) *plugin.Plugin { "aws_oam_sink": tableAwsOAMSink(ctx), "aws_opensearch_domain": tableAwsOpenSearchDomain(ctx), "aws_organizations_account": tableAwsOrganizationsAccount(ctx), + "aws_organizations_organizational_unit": tableAwsOrganizationsOrganizationalUnit(ctx), "aws_organizations_policy": tableAwsOrganizationsPolicy(ctx), "aws_organizations_policy_target": tableAwsOrganizationsPolicyTarget(ctx), + "aws_organizations_root": tableAwsOrganizationsRoot(ctx), "aws_pinpoint_app": tableAwsPinpointApp(ctx), "aws_pipes_pipe": tableAwsPipes(ctx), "aws_pricing_product": tableAwsPricingProduct(ctx), diff --git a/aws/table_aws_organizations_account.go b/aws/table_aws_organizations_account.go index 23c38d36d..c749de384 100644 --- a/aws/table_aws_organizations_account.go +++ b/aws/table_aws_organizations_account.go @@ -25,7 +25,10 @@ func tableAwsOrganizationsAccount(_ context.Context) *plugin.Table { }, List: &plugin.ListConfig{ Hydrate: listOrganizationsAccounts, - Tags: map[string]string{"service": "organizations", "action": "ListAccounts"}, + KeyColumns: plugin.KeyColumnSlice{ + {Name: "parent_id", Require: plugin.Optional}, + }, + Tags: map[string]string{"service": "organizations", "action": "ListAccounts"}, }, HydrateConfig: []plugin.HydrateConfig{ { @@ -45,6 +48,12 @@ func tableAwsOrganizationsAccount(_ context.Context) *plugin.Table { Description: "The unique identifier (account ID) of the member account.", Type: proto.ColumnType_STRING, }, + { + Name: "parent_id", + Description: "The unique identifier (ID) for the parent root or organization unit (OU) whose accounts you want to list.", + Type: proto.ColumnType_STRING, + Transform: transform.FromQual("parent_id"), + }, { Name: "arn", Description: "The Amazon Resource Name (ARN) of the account.", @@ -104,6 +113,8 @@ func tableAwsOrganizationsAccount(_ context.Context) *plugin.Table { //// LIST FUNCTION func listOrganizationsAccounts(ctx context.Context, d *plugin.QueryData, _ *plugin.HydrateData) (interface{}, error) { + parentId := d.EqualsQualString("parent_id") + // Get Client svc, err := OrganizationClient(ctx, d) if err != nil { @@ -113,8 +124,7 @@ func listOrganizationsAccounts(ctx context.Context, d *plugin.QueryData, _ *plug // The maximum number for MaxResults parameter is not defined by the API // We have set the MaxResults to 1000 based on our test - maxItems := int32(20) - params := &organizations.ListAccountsInput{} + maxItems := int32(1000) // Reduce the basic request limit down if the user has only requested a small number of rows if d.QueryContext.Limit != nil { @@ -128,6 +138,34 @@ func listOrganizationsAccounts(ctx context.Context, d *plugin.QueryData, _ *plug } } + // Lists the accounts in an organization that are contained by the specified target root or organizational unit (OU). + // If you specify the root, you get a list of all the accounts that aren't in any OU. + // If you specify an OU, you get a list of all the accounts in only that OU and not in any child OUs. + if parentId != "" { + maxItem := int32(20) // TODO for limit value in where clause + op, err := listOrganizationsAccountsForParent(ctx, d, svc, maxItem, &organizations.ListAccountsForParentInput{ + ParentId: &parentId, + MaxResults: &maxItems, + }) + if err != nil { + return nil, err + } + + accounts := op.([]types.Account) + for _, account := range accounts { + d.StreamListItem(ctx, account) + + // Context may get cancelled due to manual cancellation or if the limit has been reached + if d.RowsRemaining(ctx) == 0 { + return nil, nil + } + } + + return nil, nil + } + + params := &organizations.ListAccountsInput{} + params.MaxResults = &maxItems paginator := organizations.NewListAccountsPaginator(svc, params, func(o *organizations.ListAccountsPaginatorOptions) { o.Limit = maxItems @@ -157,6 +195,28 @@ func listOrganizationsAccounts(ctx context.Context, d *plugin.QueryData, _ *plug return nil, nil } +func listOrganizationsAccountsForParent(ctx context.Context, d *plugin.QueryData, svc *organizations.Client, maxItems int32, params *organizations.ListAccountsForParentInput) (interface{}, error) { + paginator := organizations.NewListAccountsForParentPaginator(svc, params, func(o *organizations.ListAccountsForParentPaginatorOptions) { + o.Limit = maxItems + o.StopOnDuplicateToken = true + }) + + var accounts []types.Account + for paginator.HasMorePages() { + // apply rate limiting + d.WaitForListRateLimit(ctx) + + output, err := paginator.NextPage(ctx) + if err != nil { + plugin.Logger(ctx).Error("aws_organizations_account.listOrganizationsAccountsForParent", "api_error", err) + return nil, err + } + + accounts = append(accounts, output.Accounts...) + } + return accounts, nil +} + //// HYDRATE FUNCTIONS func getOrganizationsAccount(ctx context.Context, d *plugin.QueryData, _ *plugin.HydrateData) (interface{}, error) { diff --git a/aws/table_aws_organizations_organizational_unit.go b/aws/table_aws_organizations_organizational_unit.go new file mode 100644 index 000000000..f2f83c30f --- /dev/null +++ b/aws/table_aws_organizations_organizational_unit.go @@ -0,0 +1,195 @@ +package aws + +import ( + "context" + "strings" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/organizations" + "github.com/aws/aws-sdk-go-v2/service/organizations/types" + + "github.com/turbot/steampipe-plugin-sdk/v5/grpc/proto" + "github.com/turbot/steampipe-plugin-sdk/v5/plugin" + "github.com/turbot/steampipe-plugin-sdk/v5/plugin/transform" +) + +// The table will return the Organizational Units for the root account if parent_id is not specified in the query parameter. +// If parent_id is specified in the query parameter then it will return the Organizational Units for the given parent. +func tableAwsOrganizationsOrganizationalUnit(_ context.Context) *plugin.Table { + return &plugin.Table{ + Name: "aws_organizations_organizational_unit", + Description: "AWS Organizations Organizational Unit", + List: &plugin.ListConfig{ + ParentHydrate: listOrganizationsRoots, + Hydrate: listOrganizationsOrganizationalUnits, + IgnoreConfig: &plugin.IgnoreConfig{ + ShouldIgnoreErrorFunc: shouldIgnoreErrors([]string{"ParentNotFoundException", "InvalidInputException"}), + }, + KeyColumns: plugin.KeyColumnSlice{ + { + Name: "parent_id", + Require: plugin.Optional, + Operators: []string{"="}, + }, + }, + }, + Columns: awsGlobalRegionColumns([]*plugin.Column{ + { + Name: "name", + Description: "The friendly name of this OU.", + Hydrate: getOrganizationsOrganizationalUnit, + Type: proto.ColumnType_STRING, + }, + { + Name: "id", + Description: "The unique identifier (ID) associated with this OU.", + Type: proto.ColumnType_STRING, + }, + { + Name: "arn", + Description: "The Amazon Resource Name (ARN) of this OU.", + Hydrate: getOrganizationsOrganizationalUnit, + Type: proto.ColumnType_STRING, + }, + { + Name: "parent_id", + Description: "The unique identifier (ID) of the root or OU whose child OUs you want to list.", + Type: proto.ColumnType_STRING, + }, + { + Name: "path", + Description: "The OU path is a string representation that uniquely identifies the hierarchical location of an Organizational Unit within the AWS Organizations structure.", + Type: proto.ColumnType_LTREE, + }, + + // Steampipe standard columns + { + Name: "title", + Description: resourceInterfaceDescription("title"), + Type: proto.ColumnType_STRING, + Hydrate: getOrganizationsOrganizationalUnit, + Transform: transform.FromField("Name"), + }, + { + Name: "akas", + Description: resourceInterfaceDescription("akas"), + Type: proto.ColumnType_JSON, + Hydrate: getOrganizationsOrganizationalUnit, + Transform: transform.FromField("Arn").Transform(transform.EnsureStringArray), + }, + }), + } +} + +type OrganizationalUnit struct { + types.OrganizationalUnit + Path string + ParentId string +} + +//// LIST FUNCTION + +func listOrganizationsOrganizationalUnits(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) { + parentId := *h.Item.(types.Root).Id + + // Check if the parentId is provided + // The unique identifier (ID) of the root or OU whose child OUs you want to list. + if d.EqualsQualString("parent_id") != "" { + parentId = d.EqualsQualString("parent_id") + } + + // empty check + if parentId == "" { + return nil, nil + } + + // Get Client + svc, err := OrganizationClient(ctx, d) + if err != nil { + plugin.Logger(ctx).Error("aws_organizations_organizational_unit.listOrganizationsOrganizationalUnits", "client_error", err) + return nil, err + } + + // Limiting the result + maxItems := int32(20) + + // Reduce the basic request limit down if the user has only requested a small number of rows + if d.QueryContext.Limit != nil { + limit := int32(*d.QueryContext.Limit) + if limit < maxItems { + maxItems = int32(limit) + } + } + + // Call the recursive function to list all nested OUs + rootPath := parentId + err = listAllNestedOUs(ctx, d, svc, parentId, maxItems, rootPath) + if err != nil { + plugin.Logger(ctx).Error("aws_organizations_organizational_unit.listOrganizationsOrganizationalUnits", "recursive_call_error", err) + return nil, err + } + + return nil, nil +} + +func listAllNestedOUs(ctx context.Context, d *plugin.QueryData, svc *organizations.Client, parentId string, maxItems int32, currentPath string) error { + params := &organizations.ListOrganizationalUnitsForParentInput{ + ParentId: aws.String(parentId), + MaxResults: &maxItems, + } + + paginator := organizations.NewListOrganizationalUnitsForParentPaginator(svc, params, func(o *organizations.ListOrganizationalUnitsForParentPaginatorOptions) { + o.Limit = maxItems + o.StopOnDuplicateToken = true + }) + + for paginator.HasMorePages() { + // apply rate limiting + output, err := paginator.NextPage(ctx) + if err != nil { + return err + } + + for _, unit := range output.OrganizationalUnits { + ouPath := strings.Replace(currentPath, "-", "_", -1) + "." + strings.Replace(*unit.Id, "-", "_", -1) + d.StreamListItem(ctx, OrganizationalUnit{unit, ouPath, parentId}) + + // Recursively list units for this child + err := listAllNestedOUs(ctx, d, svc, *unit.Id, maxItems, ouPath) + if err != nil { + return err + } + + if d.RowsRemaining(ctx) == 0 { + return nil + } + } + } + + return nil +} + +//// HYDRATE FUNCTIONS + +func getOrganizationsOrganizationalUnit(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) { + orgUnitId := *h.Item.(OrganizationalUnit).Id + + // Get Client + svc, err := OrganizationClient(ctx, d) + if err != nil { + plugin.Logger(ctx).Error("aws_organizations_organizational_unit.getOrganizationsOrganizationalUnit", "client_error", err) + return nil, err + } + + params := &organizations.DescribeOrganizationalUnitInput{ + OrganizationalUnitId: aws.String(orgUnitId), + } + + op, err := svc.DescribeOrganizationalUnit(ctx, params) + if err != nil { + plugin.Logger(ctx).Error("aws_organizations_organizational_unit.getOrganizationsOrganizationalUnit", "api_error", err) + return nil, err + } + + return *op.OrganizationalUnit, nil +} diff --git a/aws/table_aws_organizations_root.go b/aws/table_aws_organizations_root.go new file mode 100644 index 000000000..f44e6cd47 --- /dev/null +++ b/aws/table_aws_organizations_root.go @@ -0,0 +1,115 @@ +package aws + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/service/organizations" + + "github.com/turbot/steampipe-plugin-sdk/v5/grpc/proto" + "github.com/turbot/steampipe-plugin-sdk/v5/plugin" + "github.com/turbot/steampipe-plugin-sdk/v5/plugin/transform" +) + +// The table will return the result if the account is a member of an organization. +// You must use the credentials of an account that belongs to an organization. +// The table will return an empty row if the account isn't a member of an organization instead of AWSOrganizationsNotInUseException. +func tableAwsOrganizationsRoot(_ context.Context) *plugin.Table { + return &plugin.Table{ + Name: "aws_organizations_root", + Description: "AWS Organizations Root", + List: &plugin.ListConfig{ + Hydrate: listOrganizationsRoots, + IgnoreConfig: &plugin.IgnoreConfig{ + ShouldIgnoreErrorFunc: shouldIgnoreErrors([]string{"AWSOrganizationsNotInUseException"}), + }, + }, + Columns: awsGlobalRegionColumns([]*plugin.Column{ + { + Name: "name", + Description: "The friendly name of the root.", + Type: proto.ColumnType_STRING, + }, + { + Name: "id", + Description: "The unique identifier (ID) for the root.", + Type: proto.ColumnType_STRING, + }, + { + Name: "arn", + Description: "The Amazon Resource Name (ARN) of the root.", + Type: proto.ColumnType_STRING, + }, + { + Name: "policy_types", + Description: "The types of policies that are currently enabled for the root and therefore can be attached to the root or to its OUs or accounts.", + Type: proto.ColumnType_JSON, + }, + + // Steampipe standard columns + { + Name: "title", + Description: resourceInterfaceDescription("title"), + Type: proto.ColumnType_STRING, + Transform: transform.FromField("Name"), + }, + { + Name: "akas", + Description: resourceInterfaceDescription("akas"), + Type: proto.ColumnType_JSON, + Transform: transform.FromField("Arn").Transform(transform.EnsureStringArray), + }, + }), + } +} + +//// LIST FUNCTION + +func listOrganizationsRoots(ctx context.Context, d *plugin.QueryData, _ *plugin.HydrateData) (interface{}, error) { + + // Get Client + svc, err := OrganizationClient(ctx, d) + if err != nil { + plugin.Logger(ctx).Error("aws_organizations_root.listOrganizationsRoots", "client_error", err) + return nil, err + } + + // Limiting the result + maxItems := int32(20) + + // Reduce the basic request limit down if the user has only requested a small number of rows + if d.QueryContext.Limit != nil { + limit := int32(*d.QueryContext.Limit) + if limit < maxItems { + maxItems = int32(limit) + } + } + + params := &organizations.ListRootsInput{ + MaxResults: &maxItems, + } + + paginator := organizations.NewListRootsPaginator(svc, params, func(o *organizations.ListRootsPaginatorOptions) { + o.Limit = maxItems + o.StopOnDuplicateToken = true + }) + + for paginator.HasMorePages() { + // apply rate limiting + output, err := paginator.NextPage(ctx) + if err != nil { + plugin.Logger(ctx).Error("aws_organizations_root.listOrganizationsRoots", "api_error", err) + return nil, err + } + + for _, root := range output.Roots { + d.StreamListItem(ctx, root) + + // Context may get cancelled due to manual cancellation or if the limit has been reached + if d.RowsRemaining(ctx) == 0 { + return nil, nil + } + } + } + + return nil, nil +} diff --git a/docs/tables/aws_organizations_organizational_unit.md b/docs/tables/aws_organizations_organizational_unit.md new file mode 100644 index 000000000..a287fa6e3 --- /dev/null +++ b/docs/tables/aws_organizations_organizational_unit.md @@ -0,0 +1,187 @@ +# Table: aws_organizations_organizational_unit + +A container for accounts within a root. An OU also can contain other OUs, enabling you to create a hierarchy that resembles an upside-down tree, with a root at the top and branches of OUs that reach down, ending in accounts that are the leaves of the tree. When you attach a policy to one of the nodes in the hierarchy, it flows down and affects all the branches (OUs) and leaves (accounts) beneath it. An OU can have exactly one parent, and currently each account can be a member of exactly one OU. + +To represent the hierarchical structure, the table includes a `path` column. This column is crucial for understanding the relationship between different OUs in the hierarchy. Due to compatibility issues with the ltree type, which is typically used for representing tree-like structures in PostgreSQL, the standard hyphen (-) in the path values has been replaced with an underscore (\_). This modification ensures proper functionality of the ltree operations and queries. + +By default, querying the table without any specific filters will return all OUs from the root of the hierarchy. Users have the option to query the table using a specific `parent_id`. This allows for the retrieval of all direct child OUs under the specified parent. + +## Examples + +### Basic info +This query helps AWS administrators and cloud architects to efficiently manage, audit, and report on the structure and composition of their AWS Organizations. + +```sql+postgres +select + name, + id, + arn, + parent_id, + title, + akas +from + aws_organizations_organizational_unit; +``` + +```sql+sqlite +select + name, + id, + arn, + parent_id, + title, + akas +from + aws_organizations_organizational_unit; +``` + +### Find a specific organizational unit and all its descendants +By filtering OUs based on their path, the query efficiently retrieves information about a specific subset of your organization's structure, which is particularly useful for large organizations with complex hierarchies. + +```sql+postgres +select + name, + id, + parent_id, + path +from + aws_organizations_organizational_unit +where + path <@ 'r_wxnb.ou_wxnb_m8l8t123'; +``` + +```sql+sqlite +select + name, + id, + parent_id, + path +from + aws_organizations_organizational_unit +where + path like 'r_wxnb.ou_wxnb_m8l8t123%' +``` + +### Select all organizational units at a certain level in the hierarchy +Retrieving a list of organizational units (OUs) from a structured hierarchy, specifically those that exist at a particular level. In the context of a database or a management system like AWS Organizations, this involves using a query to filter and display only the OUs that are positioned at the same depth or stage in the hierarchical structure. + +```sql+postgres +select + name, + id, + parent_id, + path +from + aws_organizations_organizational_unit +where + nlevel(path) = 3; +``` + +```sql+sqlite +select + name, + id, + parent_id, + path +from + aws_organizations_organizational_unit +where + (length(path) - length(replace(path, '.', ''))) = 2; +``` + +### Get all ancestors of a given organizational unit +Ancestors are the units in the hierarchy that precede the given OU. An ancestor can be a direct parent (the immediate higher-level unit), or it can be any higher-level unit up to the root of the hierarchy. + +```sql+postgres +select + name, + id, + parent_id, + path +from + aws_organizations_organizational_unit +where + 'r_wxnb.ou_wxnb_m8l123aq.ou_wxnb_5gri123b' @> path; +``` + +```sql+sqlite +select + name, + id, + parent_id, + path +from + aws_organizations_organizational_unit +where + path like 'r_wxnb.ou_wxnb_m8l123aq.ou_wxnb_5gri123b%'; +``` + +### Retrieve all siblings of a specific organizational unit +The query is useful for retrieving information about sibling organizational units corresponding to a specified organizational unit. + +```sql+postgres +select + name, + id, + parent_id, + path +from + aws_organizations_organizational_unit +where + parent_id = + ( + select + parent_id + from + aws_organizations_organizational_unit + where + name = 'Punisher' + ); +``` + +```sql+sqlite +select + name, + id, + parent_id, + path +from + aws_organizations_organizational_unit +where + parent_id = + ( + select + parent_id + from + aws_organizations_organizational_unit + where + name = 'Punisher' + ); +``` + +### Select organizational units with a path that matches a specific pattern +This query is designed to retrieve organizational units that have a specific hierarchical path pattern within an AWS (Amazon Web Services) organization's structure. + +```sql+postgres +select + name, + id, + parent_id, + path +from + aws_organizations_organizational_unit +where + path ~ 'r_wxnb.*.ou_wxnb_m81234aq.*'; +``` + +```sql+sqlite +select + name, + id, + parent_id, + path +from + aws_organizations_organizational_unit +where + path like 'r_wxnb%ou_wxnb_m81234aq%'; +``` diff --git a/docs/tables/aws_organizations_root.md b/docs/tables/aws_organizations_root.md new file mode 100644 index 000000000..dd546e332 --- /dev/null +++ b/docs/tables/aws_organizations_root.md @@ -0,0 +1,60 @@ +--- +title: "Steampipe Table: aws_organizations_root - Query AWS Organizations Root using SQL" +description: "Allows users to query AWS Organizations Root to retrieve detailed information on AWS Organizations Root account. This table can be utilized to gain insights on organizations root account." +--- + +# Table: aws_organizations_root - Query AWS Organizations Root using SQL + +AWS Organizations uses a hierarchical structure to manage accounts. At the top of this hierarchy is the "root." The root is the starting point for organizing your AWS accounts. The root acts as the parent container for all the accounts in your organization. It can also contain organizational units (OUs), which are sub-containers that can themselves contain accounts or further nested OUs. + +## Table Usage Guide + +The `aws_organizations_root` table in Steampipe provides you the information about AWS Organizations Root Account. + +## Examples + +### Basic info +It's particularly useful in contexts where managing or auditing AWS Organizations. + +```sql+postgres +select + name, + id, + arn +from + aws_organizations_root; +``` + +```sql+sqlite +select + name, + id, + arn +from + aws_organizations_root; +``` + +### Get the policy details attached to organization root account +The types of policies that are currently enabled for the root and therefore can be attached to the root or to its OUs or accounts. + +```sql+postgres +select + id, + name, + p ->> 'Status' as policy_status, + p ->> 'Type' as policy_type +from + aws_organizations_root, + jsonb_array_elements(policy_types) as p; +``` + +```sql+sqlite +select + id, + name, + json_extract(json_each.value, '$.Status') AS policy_status, + json_extract(json_each.value, '$.Type') AS policy_type +from + aws_organizations_root, + json_each(policy_types) as p; +``` \ No newline at end of file