├── .github └── workflows │ └── update-reference.yml ├── LICENSE ├── README.md ├── cmd └── scrape-authref │ └── scrape.go ├── go.mod ├── go.sum ├── index.d.ts ├── index.js ├── package.json └── service-auth.json /.github/workflows/update-reference.yml: -------------------------------------------------------------------------------- 1 | name: update-reference 2 | on: 3 | schedule: 4 | - cron: '0 0 * * 0' 5 | jobs: 6 | update-reference: 7 | runs-on: ubuntu-latest 8 | outputs: 9 | changed: ${{ steps.push.outputs.changed }} 10 | steps: 11 | - uses: actions/checkout@v3 12 | - uses: actions/setup-go@v4 13 | with: 14 | go-version: '^1.16' 15 | - uses: actions/setup-node@v3 16 | with: 17 | node-version: '18' 18 | registry-url: 'https://registry.npmjs.org' 19 | - run: go run cmd/scrape-authref/scrape.go 20 | - id: commit 21 | continue-on-error: true 22 | run: | 23 | git config --global user.name "github-actions[bot]" 24 | git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" 25 | 26 | git add service-auth.json 27 | git commit -m "New update to service-auth.json" 28 | - id: push 29 | if: ${{ steps.commit.outcome == 'success' }} 30 | run: | 31 | npm --no-git-tag-version version patch 32 | git add package.json 33 | git commit --amend --no-edit 34 | git push 35 | npm publish 36 | echo "::set-output name=changed::yes" 37 | env: 38 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This license only applies to the software in this repository. I make no claim 2 | to the data, which belongs to Amazon Web Services, Inc. and is published under 3 | the assumption that it will be helpful to the AWS software and security 4 | communities. 5 | 6 | Copyright (c) 2021 Brian Crowell 7 | 8 | 9 | MIT License 10 | 11 | Permission is hereby granted, free of charge, to any person obtaining a copy 12 | of this software and associated documentation files (the "Software"), to deal 13 | in the Software without restriction, including without limitation the rights 14 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | copies of the Software, and to permit persons to whom the Software is 16 | furnished to do so, subject to the following conditions: 17 | 18 | The above copyright notice and this permission notice shall be included in all 19 | copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 27 | SOFTWARE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AWS service authorization reference 2 | 3 | This is a JSON-formatted scrape of the [AWS Service Authorization Reference](https://docs.aws.amazon.com/service-authorization/latest/reference/reference.html), along with a Golang program to update it. The package is updated weekly. 4 | 5 | ## NPM package 6 | 7 | If you're using the NPM package, you can use the service reference like this: 8 | 9 | ```typescript 10 | import { serviceAuth } from '@fluggo/aws-service-auth-reference'; 11 | 12 | for(const service of serviceAuth) { 13 | console.log(service.name); 14 | } 15 | ``` 16 | 17 | ## Reference 18 | 19 | The JSON file contains an array of service reference objects like this: 20 | 21 | ```javascript 22 | { 23 | // Name of the service as listed in the service authorization reference. 24 | "name": "AWS Security Token Service", 25 | 26 | // Prefix seen in IAM action statements for this service. 27 | "servicePrefix": "sts", 28 | 29 | // URL of the service authorization reference page for this service. 30 | "authReferenceHref": "https://docs.aws.amazon.com/service-authorization/latest/reference/list_awssecuritytokenservice.html", 31 | 32 | // URL of the API reference for this service, if any. 33 | "apiReferenceHref": "https://docs.aws.amazon.com/STS/latest/APIReference/", 34 | 35 | // List of actions that can be specified for this service in IAM action statements. 36 | "actions": [ 37 | { 38 | // Action name as it appears in IAM policy statements. 39 | "name": "AssumeRole", 40 | 41 | // True if this action is not actually associated with an API call. 42 | "permissionOnly": false, 43 | 44 | // URL of the API or user guide reference for this action. 45 | "referenceHref": "https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html", 46 | 47 | // Description of the action. 48 | "description": "Returns a set of temporary security credentials that you can use to access AWS resources that you might not normally have access to", 49 | 50 | // The access level classification for this action. 51 | // This can be List, Read, Write, Permissions management, or Tagging. 52 | // See https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_understand-policy-summary-access-level-summaries.html 53 | "accessLevel": "Write", 54 | 55 | // Resource types that can be specified for this action. 56 | // 57 | // If empty, you must specify all resources (`"*"`) in the policy when using this action. 58 | "resourceTypes": [ 59 | { 60 | // A type of resource that can be used with this action. 61 | "resourceType": "role", 62 | 63 | // True if at least one resource of this type is required to execute the action. 64 | "required": true, 65 | 66 | // Condition keys that can be specified for this resource type. 67 | // 68 | // If a statement specifies a condition key not on this list, 69 | // and its scope includes a resource of this type, the statement will 70 | // have no effect. 71 | "conditionKeys": [], 72 | 73 | // Additional permissions you must have in order to use the action. 74 | "dependentActions": [] 75 | } 76 | ], 77 | 78 | // Condition keys that can be specified for this action that do not depend on a resource type. 79 | "conditionKeys": [] 80 | }, 81 | // ... 82 | ], 83 | 84 | // Types of resources that can be specified for this service in IAM resource statements. 85 | // 86 | // These resources can come from other services; check the ARN to see the service type. 87 | "resourceTypes": [ 88 | { 89 | // Name of the resource type. 90 | "name": "role", 91 | 92 | // URL of the API or user guide reference for this action. 93 | "referenceHref": "https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles.html", 94 | 95 | // Pattern for ARNs for this resource type with `${placeholder}` markers. 96 | "arnPattern": "arn:${Partition}:iam::${Account}:role/${RoleNameWithPath}", 97 | 98 | // List of condition keys that are valid for this resource type. 99 | "conditionKeys": [ 100 | "aws:ResourceTag/${TagKey}" 101 | ] 102 | }, 103 | // ... 104 | ], 105 | 106 | // Condition keys that can be specified for this service in IAM statements. 107 | "conditionKeys": [ 108 | { 109 | // Name of the condition key, which may contain a template (`${param}`) element. 110 | "name": "sts:SourceIdentity", 111 | 112 | // Link to reference information about the condition key. 113 | "referenceHref": "https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_iam-condition-keys.html#ck_sourceidentity", 114 | 115 | // A short description of the condition key. 116 | "description": "Filters actions based on the source identity that is passed in the request", 117 | 118 | // The type of the condition key. 119 | // This can be a primitive type such as String or a compound type such as ArrayOfString. 120 | "type": "String" 121 | }, 122 | // ... 123 | ] 124 | } 125 | ``` 126 | 127 | ## Using with curl and jq 128 | 129 | You can use [curl](https://curl.se/) and [jq](https://stedolan.github.io/jq/) in shell scripts to parse the service auth JSON file and query it. For example, to find all IAM actions ending in "Role": 130 | 131 | ```bash 132 | curl --silent --show-error \ 133 | --url 'https://raw.githubusercontent.com/fluggo/aws-service-auth-reference/master/service-auth.json' \ 134 | > /tmp/auth.json 135 | 136 | cat /tmp/auth.json | jq --raw-output ' 137 | .[] 138 | | {service: .servicePrefix} + (.actions[] | {action: .name}) 139 | | select(.service == "iam" and (.action | match("Role$"))) 140 | | "\(.service):\(.action)" 141 | ' 142 | ``` 143 | 144 | Output: 145 | 146 | ```text 147 | iam:CreateRole 148 | iam:CreateServiceLinkedRole 149 | iam:DeleteRole 150 | iam:DeleteServiceLinkedRole 151 | iam:GetRole 152 | iam:ListInstanceProfilesForRole 153 | iam:PassRole 154 | iam:TagRole 155 | iam:UntagRole 156 | iam:UpdateRole 157 | ``` 158 | 159 | Example provided by @iainelder. 160 | 161 | -------------------------------------------------------------------------------- /cmd/scrape-authref/scrape.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | "net/url" 9 | "os" 10 | "regexp" 11 | "strconv" 12 | "strings" 13 | 14 | "github.com/andybalholm/cascadia" 15 | "golang.org/x/net/html" 16 | ) 17 | 18 | const ( 19 | startPage = "https://docs.aws.amazon.com/service-authorization/latest/reference/reference_policies_actions-resources-contextkeys.html" 20 | testActionsPage = "https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazonec2.html" 21 | ) 22 | 23 | var ( 24 | spaceReplacer = regexp.MustCompile(`\s{2,}`) 25 | ) 26 | 27 | func mustParseSelector(sel string) cascadia.SelectorGroup { 28 | result, err := cascadia.ParseGroup(sel) 29 | 30 | if err != nil { 31 | panic(err) 32 | } 33 | 34 | return result 35 | } 36 | 37 | func gatherText(node *html.Node, recursive bool) string { 38 | result := "" 39 | 40 | for childNode := node.FirstChild; childNode != nil; childNode = childNode.NextSibling { 41 | if childNode.Type == html.TextNode { 42 | result += childNode.Data 43 | } else if recursive { 44 | result += gatherText(childNode, true) 45 | } 46 | } 47 | 48 | return spaceReplacer.ReplaceAllLiteralString(strings.TrimSpace(result), " ") 49 | } 50 | 51 | func renderToString(node *html.Node) string { 52 | if node == nil { 53 | return "" 54 | } 55 | 56 | var buf bytes.Buffer 57 | html.Render(&buf, node) 58 | return buf.String() 59 | } 60 | 61 | func fetchHtml(url string) (*html.Node, error) { 62 | resp, err := http.Get(url) 63 | 64 | if err != nil { 65 | return nil, fmt.Errorf("HTTP GET: %w", err) 66 | } 67 | 68 | if resp.StatusCode != 200 { 69 | return nil, fmt.Errorf("HTTP GET: status code %v", resp.StatusCode) 70 | } 71 | 72 | node, err := html.Parse(resp.Body) 73 | 74 | if err != nil { 75 | return nil, fmt.Errorf("parse HTML: %w", err) 76 | } 77 | 78 | return node, nil 79 | } 80 | 81 | type topic struct { 82 | name string 83 | url *url.URL 84 | } 85 | 86 | func getAttrValue(node *html.Node, name string) string { 87 | for _, v := range node.Attr { 88 | if v.Key == name { 89 | return v.Val 90 | } 91 | } 92 | 93 | return "" 94 | } 95 | 96 | func parseTopics() ([]topic, error) { 97 | node, err := fetchHtml(startPage) 98 | 99 | if err != nil { 100 | return nil, fmt.Errorf("parseTopics: %w", err) 101 | } 102 | 103 | // Not fully documented in cascadia, but it has these additional text selectors: 104 | // 105 | // :contains("str") Selects nodes that contain the given text when all descendant text nodes are combined 106 | // :containsOwn("str") Selects nodes that contain the given text when all child text nodes are combined 107 | // :matches(^[a-z]$) Selects nodes that match the given regex when all descendant text nodes are combined 108 | // :matchesOwn(^[a-z]$) Selects nodes that match the given regex when all child text nodes are combined 109 | // :has(selector) Selects nodes that contain descendant nodes that match the given selector 110 | // :haschild(selector) Selects nodes that contain child nodes that match the given selector 111 | // :input Selects any input element (input, select, textarea, or button) 112 | // [attr#=(^[a-z]$)] Selects elements with attributes that match the given regex 113 | // 114 | // Additionally, it implements all the tree-structural pseudo-classes found here: 115 | // https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-classes#tree-structural_pseudo-classes 116 | 117 | topicsListSelector := mustParseSelector(`h6:matchesOwn(^\s*Topics\s*$) + ul`) 118 | topicsListNode := cascadia.Query(node, topicsListSelector) 119 | 120 | if topicsListNode == nil { 121 | return nil, fmt.Errorf("get topics: could not find topics") 122 | } 123 | 124 | result := make([]topic, 0, 20) 125 | baseUrl, err := url.Parse(startPage) 126 | 127 | if err != nil { 128 | panic(err) 129 | } 130 | 131 | topicsSelector := mustParseSelector(`li > a`) 132 | topicsNodes := cascadia.QueryAll(topicsListNode, topicsSelector) 133 | 134 | for _, aNode := range topicsNodes { 135 | partialHref := getAttrValue(aNode, "href") 136 | title := aNode.FirstChild.Data 137 | 138 | if partialHref == "" { 139 | return nil, fmt.Errorf("get topics: could not find topic href") 140 | } 141 | 142 | newUrl, err := baseUrl.Parse(partialHref) 143 | 144 | if err != nil { 145 | return nil, fmt.Errorf("get topics: parse URL %s: %w", partialHref, err) 146 | } 147 | 148 | result = append(result, topic{name: title, url: newUrl}) 149 | } 150 | 151 | return result, nil 152 | } 153 | 154 | type ServiceAuthorizationReference struct { 155 | Name string `json:"name"` 156 | ServicePrefix string `json:"servicePrefix"` 157 | AuthReferenceHref string `json:"authReferenceHref"` 158 | ApiReferenceHref string `json:"apiReferenceHref,omitempty"` 159 | Actions []*Action `json:"actions"` 160 | ResourceTypes []*ResourceType `json:"resourceTypes"` 161 | ConditionKeys []*ConditionKey `json:"conditionKeys"` 162 | } 163 | 164 | type ActionResourceType struct { 165 | ResourceType string `json:"resourceType"` 166 | Required bool `json:"required"` 167 | ConditionKeys []string `json:"conditionKeys"` 168 | DependentActions []string `json:"dependentActions"` 169 | } 170 | 171 | type Action struct { 172 | Name string `json:"name"` 173 | PermissionOnly bool `json:"permissionOnly"` 174 | ReferenceHref string `json:"referenceHref,omitempty"` 175 | Description string `json:"description"` 176 | AccessLevel string `json:"accessLevel"` 177 | ResourceTypes []ActionResourceType `json:"resourceTypes"` 178 | ConditionKeys []string `json:"conditionKeys"` 179 | } 180 | 181 | func parseAPIReferenceHref(page *html.Node) string { 182 | apiReferenceLink := mustParseSelector(`#main-col-body a[href]:containsOwn("API operations available for")`) 183 | 184 | if apiReferenceNode := cascadia.Query(page, apiReferenceLink); apiReferenceNode != nil { 185 | return getAttrValue(apiReferenceNode, "href") 186 | } else { 187 | return "" 188 | } 189 | } 190 | 191 | func parseServicePrefix(page *html.Node) string { 192 | servicePrefixSelector := mustParseSelector(`#main-col-body > p:containsOwn("service prefix:") > code[class*="code"]`) 193 | servicePrefixNode := cascadia.Query(page, servicePrefixSelector) 194 | 195 | return servicePrefixNode.FirstChild.Data 196 | } 197 | 198 | func parseActionsTable(page *html.Node) ([]*Action, error) { 199 | actionTableSelector := mustParseSelector(`h2:containsOwn("Actions defined by") ~ div[class*="table-container"] table`) 200 | actionTableNode := cascadia.Query(page, actionTableSelector) 201 | 202 | rowSelector := mustParseSelector(`tr`) 203 | rowNodes := cascadia.QueryAll(actionTableNode, rowSelector) 204 | 205 | cellSelector := mustParseSelector(`td`) 206 | aHrefSelector := mustParseSelector(`a[href]`) 207 | pSelector := mustParseSelector(`p`) 208 | actions := make([]*Action, 0) 209 | var action *Action 210 | var nextActionRow, nextDescriptionRow int 211 | 212 | for row := 1; row < len(rowNodes); row++ { 213 | rowNode := rowNodes[row] 214 | rowCellNodes := cascadia.QueryAll(rowNode, cellSelector) 215 | 216 | if action == nil || row == nextActionRow { 217 | action = &Action{} 218 | actions = append(actions, action) 219 | 220 | if len(rowCellNodes) != 6 { 221 | return nil, fmt.Errorf("first row of action table entry has %d cells (expected 6): %#v", len(rowCellNodes), renderToString(rowNode)) 222 | } 223 | 224 | actionRowspan := 1 225 | 226 | if rowspanValue := getAttrValue(rowCellNodes[0], "rowspan"); rowspanValue != "" { 227 | if v, err := strconv.Atoi(rowspanValue); err == nil { 228 | actionRowspan = v 229 | } 230 | } 231 | 232 | nextActionRow = row + actionRowspan 233 | nextDescriptionRow = row 234 | actionNameRaw := gatherText(rowCellNodes[0], true) 235 | actionNameSubstrings := strings.SplitN(actionNameRaw, " ", 2) 236 | 237 | if actionNameNode := cascadia.Query(rowCellNodes[0], aHrefSelector); actionNameNode != nil { 238 | action.Name = gatherText(actionNameNode, true) 239 | action.ReferenceHref = getAttrValue(actionNameNode, "href") 240 | } else { 241 | action.Name = actionNameSubstrings[0] 242 | } 243 | 244 | if strings.Contains(actionNameRaw, "[permission only]") { 245 | action.PermissionOnly = true 246 | } 247 | 248 | action.ResourceTypes = make([]ActionResourceType, 0) 249 | action.ConditionKeys = make([]string, 0) 250 | } 251 | 252 | if row == nextDescriptionRow { 253 | descriptionRowspan := 1 254 | descriptionCellNode := rowCellNodes[len(rowCellNodes)-5] 255 | 256 | if rowspanValue := getAttrValue(descriptionCellNode, "rowspan"); rowspanValue != "" { 257 | if v, err := strconv.Atoi(rowspanValue); err == nil { 258 | descriptionRowspan = v 259 | } 260 | } 261 | 262 | nextDescriptionRow = row + descriptionRowspan 263 | 264 | // For now, we only take the first description we find; the "SCENARIO" blocks in the EC2 documentation aren't interesting to us 265 | if action.Description != "" { 266 | row = nextActionRow - 1 267 | continue 268 | } 269 | 270 | action.Description = gatherText(descriptionCellNode, true) 271 | 272 | accessLevelNode := rowCellNodes[len(rowCellNodes)-4] 273 | action.AccessLevel = gatherText(accessLevelNode, true) 274 | } 275 | 276 | conditionKeyNodes := cascadia.QueryAll(rowCellNodes[len(rowCellNodes)-2], pSelector) 277 | conditionKeys := make([]string, len(conditionKeyNodes)) 278 | for k, conditionKeyNode := range conditionKeyNodes { 279 | conditionKeys[k] = gatherText(conditionKeyNode, true) 280 | } 281 | 282 | resourceTypeField := gatherText(rowCellNodes[len(rowCellNodes)-3], true) 283 | if resourceTypeField == "" { 284 | action.ConditionKeys = conditionKeys 285 | continue 286 | } 287 | 288 | resourceType := ActionResourceType{} 289 | resourceType.ResourceType = strings.TrimSuffix(resourceTypeField, "*") 290 | resourceType.Required = strings.HasSuffix(resourceTypeField, "*") 291 | resourceType.ConditionKeys = conditionKeys 292 | 293 | dependentActionNodes := cascadia.QueryAll(rowCellNodes[len(rowCellNodes)-1], pSelector) 294 | resourceType.DependentActions = make([]string, len(dependentActionNodes)) 295 | 296 | for k, dependentActionNode := range dependentActionNodes { 297 | resourceType.DependentActions[k] = gatherText(dependentActionNode, true) 298 | } 299 | 300 | action.ResourceTypes = append(action.ResourceTypes, resourceType) 301 | } 302 | 303 | return actions, nil 304 | } 305 | 306 | type ResourceType struct { 307 | Name string `json:"name"` 308 | ReferenceHref string `json:"referenceHref,omitempty"` 309 | ArnPattern string `json:"arnPattern"` 310 | ConditionKeys []string `json:"conditionKeys"` 311 | } 312 | 313 | func parseResourceTypesTable(page *html.Node) []*ResourceType { 314 | rtTableSelector := mustParseSelector(`h2:containsOwn("Resource types defined by") + p + div[class*="table-container"] table, h2:containsOwn("Resource types defined by") + p + div + div[class*="table-container"] table`) 315 | rtTableNode := cascadia.Query(page, rtTableSelector) 316 | 317 | if rtTableNode == nil { 318 | return make([]*ResourceType, 0) 319 | } 320 | 321 | rowSelector := mustParseSelector(`tr`) 322 | rowNodes := cascadia.QueryAll(rtTableNode, rowSelector) 323 | 324 | cellSelector := mustParseSelector(`td`) 325 | aHrefSelector := mustParseSelector(`a[href]`) 326 | pSelector := mustParseSelector(`p`) 327 | resourceTypes := make([]*ResourceType, 0) 328 | var resourceType *ResourceType 329 | 330 | for row := 1; row < len(rowNodes); row++ { 331 | rowNode := rowNodes[row] 332 | rowCellNodes := cascadia.QueryAll(rowNode, cellSelector) 333 | 334 | resourceType = &ResourceType{} 335 | resourceTypes = append(resourceTypes, resourceType) 336 | 337 | if len(rowCellNodes) != 3 { 338 | panic(fmt.Errorf("first row of resource table entry has %d cells (expected 3): %#v", len(rowCellNodes), renderToString(rowNode))) 339 | } 340 | 341 | resourceType.Name = gatherText(rowCellNodes[0], true) 342 | 343 | if resourceTypeRefLink := cascadia.Query(rowCellNodes[0], aHrefSelector); resourceTypeRefLink != nil { 344 | resourceType.ReferenceHref = getAttrValue(resourceTypeRefLink, "href") 345 | } 346 | 347 | resourceType.ArnPattern = gatherText(rowCellNodes[1], true) 348 | 349 | conditionKeyNodes := cascadia.QueryAll(rowCellNodes[2], pSelector) 350 | resourceType.ConditionKeys = make([]string, len(conditionKeyNodes)) 351 | 352 | for k, conditionKeyNode := range conditionKeyNodes { 353 | resourceType.ConditionKeys[k] = gatherText(conditionKeyNode, true) 354 | } 355 | } 356 | 357 | return resourceTypes 358 | } 359 | 360 | type ConditionKey struct { 361 | Name string `json:"name"` 362 | ReferenceHref string `json:"referenceHref,omitempty"` 363 | Description string `json:"description"` 364 | Type string `json:"type"` 365 | } 366 | 367 | func parseConditionKeyTable(page *html.Node) []*ConditionKey { 368 | ckTableSelector := mustParseSelector(`h2:containsOwn("Condition keys for") + p + p + div[class*="table-container"] table`) 369 | ckTableNode := cascadia.Query(page, ckTableSelector) 370 | 371 | if ckTableNode == nil { 372 | return make([]*ConditionKey, 0) 373 | } 374 | 375 | rowSelector := mustParseSelector(`tr`) 376 | rowNodes := cascadia.QueryAll(ckTableNode, rowSelector) 377 | 378 | cellSelector := mustParseSelector(`td`) 379 | aHrefSelector := mustParseSelector(`a[href]`) 380 | // pSelector := mustParseSelector(`p`) 381 | conditionKeys := make([]*ConditionKey, 0) 382 | var conditionKey *ConditionKey 383 | 384 | for row := 1; row < len(rowNodes); row++ { 385 | rowNode := rowNodes[row] 386 | rowCellNodes := cascadia.QueryAll(rowNode, cellSelector) 387 | 388 | conditionKey = &ConditionKey{} 389 | conditionKeys = append(conditionKeys, conditionKey) 390 | 391 | if len(rowCellNodes) != 3 { 392 | fmt.Printf("%s\n", renderToString(rowNode)) 393 | panic(fmt.Errorf("first row of condition key entry has %d cells (expected 3)", len(rowCellNodes))) 394 | } 395 | 396 | conditionKey.Name = gatherText(rowCellNodes[0], true) 397 | 398 | if refLink := cascadia.Query(rowCellNodes[0], aHrefSelector); refLink != nil { 399 | conditionKey.ReferenceHref = getAttrValue(refLink, "href") 400 | } 401 | 402 | conditionKey.Description = gatherText(rowCellNodes[1], true) 403 | conditionKey.Type = gatherText(rowCellNodes[2], true) 404 | } 405 | 406 | return conditionKeys 407 | } 408 | 409 | func main() { 410 | topics, err := parseTopics() 411 | 412 | if err != nil { 413 | fmt.Fprintf(os.Stderr, "failed to parse topics page: %v\n", err) 414 | os.Exit(1) 415 | } 416 | 417 | authRefs := make([]*ServiceAuthorizationReference, 0) 418 | 419 | for _, topic := range topics { 420 | page, err := fetchHtml(topic.url.String()) 421 | 422 | if err != nil { 423 | fmt.Fprintf(os.Stderr, "topic %#v: %v\n", topic.name, err) 424 | os.Exit(1) 425 | } 426 | 427 | authRef := &ServiceAuthorizationReference{Name: topic.name, AuthReferenceHref: topic.url.String()} 428 | authRefs = append(authRefs, authRef) 429 | 430 | if actions, err := parseActionsTable(page); err != nil { 431 | fmt.Fprintf(os.Stderr, "topic %#v: actions table: %v\n", topic.name, err) 432 | os.Exit(1) 433 | } else { 434 | authRef.Actions = actions 435 | } 436 | 437 | authRef.ConditionKeys = parseConditionKeyTable(page) 438 | authRef.ResourceTypes = parseResourceTypesTable(page) 439 | authRef.ApiReferenceHref = parseAPIReferenceHref(page) 440 | authRef.ServicePrefix = parseServicePrefix(page) 441 | } 442 | 443 | indentedFile, err := os.Create("service-auth.json") 444 | 445 | if err != nil { 446 | fmt.Fprintf(os.Stderr, "could not open output file: %v\n", err) 447 | os.Exit(1) 448 | } 449 | 450 | encoder := json.NewEncoder(indentedFile) 451 | encoder.SetIndent("", " ") 452 | 453 | encoder.Encode(authRefs) 454 | 455 | if err := indentedFile.Close(); err != nil { 456 | fmt.Fprintf(os.Stderr, "could not close output file: %v\n", err) 457 | os.Exit(1) 458 | } 459 | } 460 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/fluggo/aws-service-auth-reference 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/andybalholm/cascadia v1.2.0 7 | golang.org/x/net v0.0.0-20210716203947-853a461950ff 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/andybalholm/cascadia v1.2.0 h1:vuRCkM5Ozh/BfmsaTm26kbjm0mIOM3yS5Ek/F5h18aE= 2 | github.com/andybalholm/cascadia v1.2.0/go.mod h1:YCyR8vOZT9aZ1CHEd8ap0gMVm2aFgxBp0T0eFw1RUQY= 3 | golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 4 | golang.org/x/net v0.0.0-20210716203947-853a461950ff h1:j2EK/QoxYNBsXI4R7fQkkRUk8y6wnOBI+6hgPdP/6Ds= 5 | golang.org/x/net v0.0.0-20210716203947-853a461950ff/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 6 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 7 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 8 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 9 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 10 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 11 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Describes the IAM authorization details for an AWS service. 3 | */ 4 | export interface ServiceAuthorizationReference { 5 | /** 6 | * Name of the service as listed in the service authorization 7 | * reference. 8 | */ 9 | name: string; 10 | 11 | /** 12 | * Prefix seen in IAM action statements for this service. 13 | */ 14 | servicePrefix: string; 15 | 16 | /** 17 | * URL of the service authorization reference page for this service. 18 | */ 19 | authReferenceHref: string; 20 | 21 | /** 22 | * URL of the API reference for this service, if any. 23 | */ 24 | apiReferenceHref?: string; 25 | 26 | /** 27 | * List of actions that can be specified for this service in IAM action statements. 28 | */ 29 | actions: Action[]; 30 | 31 | /** 32 | * Types of resources that can be specified for this service in IAM resource statements. 33 | * 34 | * These resources can come from other services; check the ARN to see the service type. 35 | */ 36 | resourceTypes: ResourceType[]; 37 | 38 | /** 39 | * Condition keys that can be specified for this service in IAM statements. 40 | */ 41 | conditionKeys: ConditionKey[]; 42 | } 43 | 44 | /** 45 | * A action that can be allowed or denied via IAM policy. 46 | */ 47 | export interface Action { 48 | /** 49 | * Action name as it appears in IAM policy statements. 50 | */ 51 | name: string; 52 | 53 | /** 54 | * True if this action is not actually associated with an API call. 55 | */ 56 | permissionOnly: boolean; 57 | 58 | /** 59 | * URL of the API or user guide reference for this action. 60 | */ 61 | referenceHref?: string; 62 | 63 | /** 64 | * Description of the action. 65 | */ 66 | description: string; 67 | 68 | /** 69 | * The access level classification for this action. 70 | * 71 | * See the [IAM user guide](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_understand-policy-summary-access-level-summaries.html) for more information. 72 | */ 73 | accessLevel: 'List' | 'Read' | 'Write' | 'Permissions management' | 'Tagging'; 74 | 75 | /** 76 | * Resource types that can be specified for this action. 77 | * 78 | * If empty, you must specify all resources (`"*"`) in the policy when using this action. 79 | */ 80 | resourceTypes: ActionResourceType[]; 81 | } 82 | 83 | /** 84 | * A resource that can be specified on an action. 85 | */ 86 | export interface ActionResourceType { 87 | /** 88 | * A resource type that can be used with an action. 89 | */ 90 | resourceType: string; 91 | 92 | /** 93 | * True if a resource of this type is required in order to execute the action. 94 | * That is, if the IAM statement specifies resources, at least one resource of this type is required. 95 | */ 96 | required: boolean; 97 | 98 | /** 99 | * Condition keys that can be specified for this resource type. 100 | * 101 | * If a statement specifies a condition key not on this list, 102 | * and its scope includes a resource of this type, the statement will 103 | * have no effect. 104 | */ 105 | conditionKeys: string[]; 106 | 107 | /** 108 | * Additional permissions you must have in order to use the action. 109 | */ 110 | dependentActions: string[]; 111 | } 112 | 113 | /** 114 | * A type of resource that can be specified for a service in an IAM policy. 115 | */ 116 | export interface ResourceType { 117 | /** 118 | * Name of the resource type. 119 | */ 120 | name: string; 121 | 122 | /** 123 | * URL of the API or user guide reference for this action. 124 | */ 125 | referenceHref?: string; 126 | 127 | 128 | /** 129 | * Pattern for ARNs for this resource type with `${placeholder}` markers. 130 | */ 131 | arnPattern: string; 132 | 133 | /** 134 | * List of condition keys that are valid for this resource type. 135 | */ 136 | conditionKeys: string[]; 137 | } 138 | 139 | /** 140 | * A condition that can be specified for an action in an IAM policy. 141 | */ 142 | export interface ConditionKey { 143 | /** 144 | * Name of the condition key, which may contain a template (`${param}`) element. 145 | */ 146 | name: string; 147 | 148 | /** 149 | * Link to reference information about the condition key. 150 | */ 151 | referenceHref?: string; 152 | 153 | /** 154 | * A short description of the condition key. 155 | */ 156 | description: string; 157 | 158 | /** 159 | * The type of the condition key. 160 | * 161 | * This can be a primitive type such as String or a compound type such as ArrayOfString. 162 | */ 163 | type: string; 164 | } 165 | 166 | declare const serviceAuth: ServiceAuthorizationReference[]; 167 | export { serviceAuth }; 168 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const serviceAuth = require('./service-auth.json'); 4 | 5 | module.exports = { 6 | serviceAuth 7 | }; 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fluggo/aws-service-auth-reference", 3 | "version": "1.0.168", 4 | "description": "A JSON reference for AWS service authorization (IAM actions)", 5 | "main": "index.js", 6 | "types": "index.d.ts", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/fluggo/aws-service-auth-reference.git" 10 | }, 11 | "files": [ 12 | "index.js", 13 | "index.d.ts", 14 | "service-auth.json" 15 | ], 16 | "keywords": [ 17 | "aws", 18 | "iam", 19 | "reference", 20 | "service" 21 | ], 22 | "author": { 23 | "name": "Brian Crowell", 24 | "email": "brian@fluggo.com" 25 | }, 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/fluggo/aws-service-auth-reference/issues" 29 | }, 30 | "homepage": "https://github.com/fluggo/aws-service-auth-reference#readme" 31 | } 32 | --------------------------------------------------------------------------------