Skip to content
This repository has been archived by the owner on Jan 23, 2025. It is now read-only.

Commit

Permalink
Merge branch 'master' into feature/iam-permission-set
Browse files Browse the repository at this point in the history
  • Loading branch information
simar7 authored Jan 13, 2024
2 parents db068cd + 35a58ce commit 1ce14ca
Show file tree
Hide file tree
Showing 13 changed files with 6,867 additions and 3,629 deletions.
25 changes: 25 additions & 0 deletions .github/workflows/upd-allowed-actions.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Update the list of allowed actions

on:
workflow_dispatch:
schedule:
- cron: '0 0 * * *'

permissions:
contents: write

jobs:
update:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Update actions
run: make update-allowed-actions

- name: Push changes
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: 'chore(terraform): update allowed actions'
push_options: --force
20 changes: 20 additions & 0 deletions .github/workflows/verify-schema.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: verify schema
on:
pull_request:
merge_group:
jobs:
build:
name: verifying schema
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3

- uses: actions/setup-go@v3
with:
go-version-file: go.mod
cache: true
cache-dependency-path: go.sum

- run: go run ./cmd/schema verify

9 changes: 9 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,12 @@ fix-typos:
quality:
which golangci-lint || go install github.com/golangci/golangci-lint/cmd/[email protected]
golangci-lint run --timeout 3m --verbose

.PHONY: schema
schema:
go run ./cmd/schema generate

.PHONY: update-allowed-actions
update-allowed-actions:
go run ./cmd/allowed_actions

274 changes: 274 additions & 0 deletions cmd/allowed_actions/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
package main

import (
"bufio"
"context"
"errors"
"flag"
"fmt"
"log"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"time"

"github.com/antchfx/htmlquery"
"golang.org/x/net/html"
"golang.org/x/sync/errgroup"
)

const (
serviceAuthURL = "https://docs.aws.amazon.com/service-authorization/latest/reference/"
serviceActionReferencesURL = "https://docs.aws.amazon.com/service-authorization/latest/reference/reference_policies_actions-resources-contextkeys.html"
)

const targetFile = "./pkg/providers/aws/iam/actions.go"

func main() {
if err := run(); err != nil {
log.Fatal(err)
}
}

const defaultParallel = 10

func run() error {
log.Println("Start parsing actions")
startTime := time.Now()
defer func() {
log.Printf("Parsing is completed. Duration %fs", time.Since(startTime).Seconds())
}()

limit := flag.Int("limit", defaultParallel, fmt.Sprintf("number of goroutines for scraping pages (default %d)", defaultParallel))
flag.Parse()

doc, err := htmlquery.LoadURL(serviceActionReferencesURL)
if err != nil {
return fmt.Errorf("failed to retrieve action references: %w", err)
}
urls, err := parseServiceURLs(doc)
if err != nil {
return err
}

g, ctx := errgroup.WithContext(context.TODO())
g.SetLimit(*limit)

// actions may be the same for services of different versions,
// e.g. Elastic Load Balancing and Elastic Load Balancing V2
actionsSet := make(map[string]struct{})

var mu sync.Mutex

for _, url := range urls {
url := url
if ctx.Err() != nil {
break
}
g.Go(func() error {
serviceActions, err := parseActions(url)
if err != nil {
return fmt.Errorf("failed to parse actions from %q: %w", url, err)
}

mu.Lock()
for _, act := range serviceActions {
actionsSet[act] = struct{}{}
}
mu.Unlock()

return nil
})
}

if err := g.Wait(); err != nil {
return err
}

actions := make([]string, 0, len(actionsSet))

for act := range actionsSet {
actions = append(actions, act)
}

sort.Strings(actions)

path := filepath.FromSlash(targetFile)
if err := generateFile(path, actions); err != nil {
return fmt.Errorf("failed to generate file: %w", err)
}
return nil
}

func parseServiceURLs(doc *html.Node) ([]string, error) {
nodes, err := htmlquery.QueryAll(doc, `//div[@class="highlights"]/ul/li/a/@href`)
if err != nil {
return nil, fmt.Errorf("failed to search nodes: %w", err)
}

res := make([]string, 0, len(nodes))

for _, node := range nodes {
// <a href="./list_awsaccountmanagement.html">AWS Account Management</a>
if node.FirstChild != nil {
res = append(res, serviceAuthURL+node.FirstChild.Data[2:])
}
}

return res, nil
}

func parseActions(url string) ([]string, error) {

doc, err := htmlquery.LoadURL(url)
if err != nil {
return nil, err
}

servicePrefix, err := parseServicePrefix(doc)
if err != nil {
return nil, err
}

actions, err := parseServiceActions(doc)
if err != nil {
return nil, err
}

res := make([]string, 0, len(actions))

for _, act := range actions {
res = append(res, servicePrefix+":"+act)
}

log.Printf("Parsing of %q actions is completed", servicePrefix)

return res, nil
}

func parseServiceActions(doc *html.Node) ([]string, error) {
table, err := htmlquery.Query(doc, `//div[@class="table-container"]/div/table/tbody`)
if table == nil {
return nil, errors.New("actions table not found")
}
if err != nil {
return nil, fmt.Errorf("failed to query tables: %w", err)
}

var actions []string

var f func(*html.Node)
f = func(n *html.Node) {
for _, tr := range findSubtags(n, "tr") {
var action string
for k, td := range findSubtags(tr, "td") {
// first column - action
if k == 0 {
if a := findSubtag(td, "a"); a != nil && a.FirstChild != nil {
action = a.FirstChild.Data
}

// fourth column - resource type
// If the column is empty, then the action does not support resource-level permissions
// and you must specify all resources ("*") in your policy
} else if action != "" && k == 3 && td.FirstChild == nil {
actions = append(actions, action)
}
}
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
f(c)
}
}
f(table)

return actions, err
}

func findSubtag(n *html.Node, tagName string) *html.Node {
for c := n.FirstChild; c != nil; c = c.NextSibling {
if c.Type == html.ElementNode && c.Data == tagName {
return c
}
}

return nil
}

func findSubtags(n *html.Node, tagName string) []*html.Node {
result := make([]*html.Node, 0)
for c := n.FirstChild; c != nil; c = c.NextSibling {
if c.Type == html.ElementNode && c.Data == tagName {
result = append(result, c)
}
}
return result
}

func parseServicePrefix(doc *html.Node) (string, error) {
nodes, err := htmlquery.QueryAll(doc, `//div[@id="main-col-body"]/p/descendant-or-self::*/text()`)
if err != nil {
return "", fmt.Errorf("failed to query paragraph: %w", err)
}

var sb strings.Builder
for _, node := range nodes {
sb.WriteString(node.Data)
}

p := sb.String()
sb.Reset()

idx := strings.Index(p, "service prefix: ")
if idx == -1 {
return "", fmt.Errorf("failed extract service prefix from text: %s", p)
}
idx += len("service prefix: ")

if len(p)-1 <= idx {
return "", fmt.Errorf("failed to parse service prefix from text: %s", p)
}

var parsed bool
for _, r := range p[idx:] {
if r == ')' {
parsed = true
break
}
sb.WriteRune(r)
}

if !parsed {
return "", fmt.Errorf("failed to parse service prefix from text: %s", p)
}

return sb.String(), nil
}

func generateFile(path string, actions []string) error {

f, err := os.Create(path)
if err != nil {
return fmt.Errorf("failed to create file: %w", err)
}
defer f.Close()

w := bufio.NewWriter(f)
_, _ = w.WriteString(
`// Code generated by cmd/allowed_actions DO NOT EDIT.
package iam
var allowedActionsForResourceWildcardsMap = map[string]struct{}{
`,
)

for _, action := range actions {
_, _ = w.WriteString("\t\"" + action + "\": {},\n")
}
_, _ = w.WriteString("}")

return w.Flush()
}
Loading

0 comments on commit 1ce14ca

Please sign in to comment.