* moves resource-table.mdx to internal/website/permstable * updates location of generated resource-table.mdx file to internal/website/permstable/ This change facilitates the implementation of the web-unified-docs content migration, which will need to source generated content like this table from outside the website folder after the content migration.
472 lines
12 KiB
Go
472 lines
12 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
|
|
// Import the ratelimiter logic for the side effect of getting all service
|
|
// handlers imported and their resources and actions registered.
|
|
_ "github.com/hashicorp/boundary/internal/ratelimit"
|
|
"github.com/hashicorp/boundary/internal/types/action"
|
|
"github.com/hashicorp/boundary/internal/types/resource"
|
|
)
|
|
|
|
const permsFile = "internal/website/permstable/resource-table.mdx"
|
|
|
|
var (
|
|
iamScopes = []string{"Global", "Org"}
|
|
infraScope = []string{"Project"}
|
|
tableHeaders = []string{
|
|
"API endpoint",
|
|
"Parameters into permissions engine",
|
|
"Available actions / examples",
|
|
}
|
|
)
|
|
|
|
type Page struct {
|
|
Resources []*Resource
|
|
}
|
|
|
|
type Resource struct {
|
|
Type string
|
|
Scopes []string
|
|
Endpoints []*Endpoint
|
|
}
|
|
|
|
type Endpoint struct {
|
|
Path string
|
|
Params map[string]string
|
|
Actions []*Action
|
|
}
|
|
|
|
type Action struct {
|
|
Name string
|
|
Description string
|
|
Examples []string
|
|
}
|
|
|
|
var page = &Page{
|
|
Resources: make([]*Resource, 0, 12),
|
|
}
|
|
|
|
func main() {
|
|
var orderedResources []resource.Type
|
|
for _, res := range resource.Map {
|
|
orderedResources = append(orderedResources, res)
|
|
}
|
|
slices.SortFunc(orderedResources, func(a, b resource.Type) int {
|
|
return strings.Compare(a.String(), b.String())
|
|
})
|
|
|
|
for _, res := range orderedResources {
|
|
switch res {
|
|
case resource.Unknown, resource.All, resource.Controller:
|
|
continue
|
|
}
|
|
info := resources[res]
|
|
|
|
name := strings.Replace(res.String(), "-", " ", 1)
|
|
singularName := name
|
|
switch []rune(strings.ToLower(singularName))[0] {
|
|
case 'a', 'e', 'i', 'o':
|
|
// 'u' is not included since our only u word is 'user' which
|
|
// should use an 'a'.
|
|
singularName = "an " + singularName
|
|
default:
|
|
singularName = "a " + singularName
|
|
}
|
|
|
|
var pin string
|
|
if parent := res.Parent(); parent != res {
|
|
pin = parent.String()
|
|
}
|
|
collectionEndpoints := &Endpoint{
|
|
Path: fmt.Sprintf("/%s", res.PluralString()),
|
|
Params: map[string]string{
|
|
"Type": res.String(),
|
|
},
|
|
}
|
|
colActions, err := action.CollectionActionSetForResource(res)
|
|
if err != nil {
|
|
panic("This shouldn't happen!")
|
|
}
|
|
for a := range colActions {
|
|
actionName := a.String()
|
|
examples := []string{
|
|
fmt.Sprintf("type=<type>;actions=%s", actionName),
|
|
}
|
|
if strings.Contains(actionName, ":") {
|
|
parentActionName := strings.SplitN(actionName, ":", 1)[0]
|
|
examples = append([]string{fmt.Sprintf("type=<type>;actions=%s", parentActionName)}, examples...)
|
|
}
|
|
collectionEndpoints.Actions = append(collectionEndpoints.Actions, &Action{
|
|
Name: a.String(),
|
|
Examples: examples,
|
|
Description: info.description(a, singularName),
|
|
})
|
|
}
|
|
slices.SortFunc(collectionEndpoints.Actions, func(a, b *Action) int {
|
|
return strings.Compare(a.Name, b.Name)
|
|
})
|
|
|
|
idEndpoints := &Endpoint{
|
|
Path: fmt.Sprintf("/%s/<id>", res.PluralString()),
|
|
Params: map[string]string{
|
|
"ID": "<id>",
|
|
"Type": res.String(),
|
|
},
|
|
}
|
|
if pin != "" {
|
|
idEndpoints.Params["Pin"] = fmt.Sprintf("<%s-id>", pin)
|
|
}
|
|
idActionSet, err := action.IdActionSetForResource(res)
|
|
if err != nil {
|
|
panic("This shouldn't happen!")
|
|
}
|
|
var idActions []action.Type
|
|
for a := range idActionSet {
|
|
idActions = append(idActions, a)
|
|
}
|
|
|
|
// Always put the first actions as Read, Update, Delete in that order
|
|
weighted := map[action.Type]int{
|
|
action.Read: 100,
|
|
action.Update: 90,
|
|
action.Delete: 80,
|
|
}
|
|
slices.SortFunc(idActions, func(a, b action.Type) int {
|
|
aWeight := weighted[a]
|
|
bWeight := weighted[b]
|
|
return strings.Compare(a.String(), b.String()) - aWeight + bWeight
|
|
})
|
|
|
|
for _, a := range idActions {
|
|
if a == action.NoOp {
|
|
continue
|
|
}
|
|
examples := []string{
|
|
fmt.Sprintf("ids=<id>;actions=%s", a.String()),
|
|
}
|
|
if pin != "" {
|
|
examples = append(examples, fmt.Sprintf("ids=<pin>;type=<type>;actions=%s", a.String()))
|
|
}
|
|
idEndpoints.Actions = append(idEndpoints.Actions, &Action{
|
|
Name: a.String(),
|
|
Examples: examples,
|
|
Description: info.description(a, singularName),
|
|
})
|
|
}
|
|
|
|
endpoints := make([]*Endpoint, 0, 2)
|
|
if len(collectionEndpoints.Actions) > 0 {
|
|
endpoints = append(endpoints, collectionEndpoints)
|
|
}
|
|
if len(idEndpoints.Actions) > 0 {
|
|
endpoints = append(endpoints, idEndpoints)
|
|
}
|
|
pr := &Resource{
|
|
Type: name,
|
|
Scopes: info.scopes,
|
|
Endpoints: endpoints,
|
|
}
|
|
|
|
page.Resources = append(page.Resources, pr)
|
|
}
|
|
|
|
fileContents, err := os.ReadFile(permsFile)
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
lines := strings.Split(string(fileContents), "\n")
|
|
var pre, post []string
|
|
var marker int
|
|
for i, line := range lines {
|
|
if strings.Contains(line, "BEGIN TABLE") {
|
|
marker = i
|
|
}
|
|
pre = append(pre, line)
|
|
if marker != 0 {
|
|
break
|
|
}
|
|
}
|
|
|
|
for i := marker + 1; i < len(lines); i++ {
|
|
if !strings.Contains(lines[i], "END TABLE") {
|
|
continue
|
|
}
|
|
marker = i
|
|
break
|
|
}
|
|
|
|
for i := marker; i < len(lines); i++ {
|
|
post = append(post, lines[i])
|
|
}
|
|
|
|
final := fmt.Sprintf("%s\n\n%s\n\n%s\n\n%s",
|
|
strings.Join(pre, "\n"),
|
|
strings.Join(page.MarshalTableOfContents(), "\n"),
|
|
strings.Join(page.MarshalBody(), "\n"),
|
|
strings.Join(post, "\n"))
|
|
|
|
if err := os.WriteFile(permsFile, []byte(final), 0o644); err != nil {
|
|
fmt.Println(err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func (p *Page) MarshalTableOfContents() (ret []string) {
|
|
for _, v := range p.Resources {
|
|
ret = append(ret, fmt.Sprintf(
|
|
"- [%s](#%s)",
|
|
toSentenceCase(v.Type),
|
|
strings.ReplaceAll(strings.ToLower(v.Type), " ", "-"),
|
|
))
|
|
}
|
|
|
|
return ret
|
|
}
|
|
|
|
func (p *Page) MarshalBody() (ret []string) {
|
|
for _, v := range p.Resources {
|
|
ret = append(ret, v.Marshal()...)
|
|
ret = append(ret, "")
|
|
}
|
|
|
|
return ret
|
|
}
|
|
|
|
func (r *Resource) Marshal() (ret []string) {
|
|
// Section Header
|
|
ret = append(ret, fmt.Sprintf("## %s\n", toSentenceCase(r.Type)))
|
|
|
|
// Scopes information
|
|
var scopes []string
|
|
for _, s := range r.Scopes {
|
|
scopes = append(scopes, fmt.Sprintf("**%s**", s))
|
|
}
|
|
if len(scopes) > 0 {
|
|
ret = append(ret, fmt.Sprintf(
|
|
"The **%s** resource type supports the following scopes: %s\n",
|
|
toSentenceCase(r.Type),
|
|
strings.TrimSpace(strings.Join(scopes, ", ")),
|
|
))
|
|
}
|
|
|
|
// Table Header
|
|
ret = append(ret, fmt.Sprintf("| %s |", strings.Join(tableHeaders, " | ")))
|
|
var headerSeparators []string
|
|
for _, t := range tableHeaders {
|
|
headerSeparators = append(headerSeparators, strings.Repeat("-", len(t)))
|
|
}
|
|
ret = append(ret, fmt.Sprintf("| %s |", strings.Join(headerSeparators, " | ")))
|
|
|
|
// Table Body
|
|
for _, v := range r.Endpoints {
|
|
ret = append(ret, v.Marshal()...)
|
|
}
|
|
|
|
return ret
|
|
}
|
|
|
|
func (e *Endpoint) Marshal() (ret []string) {
|
|
var row []string
|
|
|
|
// Endpoint Field
|
|
row = append(row, fmt.Sprintf("<code>%s</code>", escape(e.Path)))
|
|
|
|
// Parameters Field
|
|
pString := "<ul>"
|
|
for _, v := range sortedKeys(e.Params) {
|
|
pString = fmt.Sprintf("%s<li>%s</li>", pString, v)
|
|
pString = fmt.Sprintf("%s<ul><li><code>%s</code></li></ul>", pString, escape(e.Params[v]))
|
|
}
|
|
pString = fmt.Sprintf("%s</ul>", pString)
|
|
row = append(row, pString)
|
|
|
|
// Actions Field
|
|
aString := "<ul>"
|
|
for _, v := range e.Actions {
|
|
aString = fmt.Sprintf(
|
|
"%s<li><code>%s</code>: %s</li>",
|
|
aString,
|
|
escape(v.Name),
|
|
v.Description,
|
|
)
|
|
|
|
eString := "<ul>"
|
|
for _, x := range v.Examples {
|
|
// Intentionally using markdown code highlighting here for readability
|
|
eString = fmt.Sprintf("%s<li>`%s`</li>", eString, x)
|
|
}
|
|
eString = fmt.Sprintf("%s</ul>", eString)
|
|
|
|
aString = fmt.Sprintf("%s%s", aString, eString)
|
|
}
|
|
aString = fmt.Sprintf("%s</ul>", aString)
|
|
row = append(row, aString)
|
|
|
|
ret = append(ret, fmt.Sprintf("| %s |", strings.Join(row, " | ")))
|
|
|
|
return ret
|
|
}
|
|
|
|
func toSentenceCase(s string) string {
|
|
return fmt.Sprintf(
|
|
"%s%s",
|
|
strings.ToUpper(s[:1]), strings.ToLower(s[1:]),
|
|
)
|
|
}
|
|
|
|
func escape(s string) string {
|
|
ret := strings.Replace(s, "<", "<", -1)
|
|
return strings.Replace(ret, ">", ">", -1)
|
|
}
|
|
|
|
func sortedKeys(in map[string]string) []string {
|
|
out := make([]string, 0, len(in))
|
|
for k := range in {
|
|
out = append(out, k)
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
// info holds information for a specific resource
|
|
type info struct {
|
|
// The scopes this resource can be in
|
|
scopes []string
|
|
// If the auto generated descriptions do not correctly cover these actions
|
|
// for this resource, including the action and a description here will
|
|
// cause this to be used instead of the auto generated one.
|
|
actionDescOverrides map[action.Type]string
|
|
}
|
|
|
|
// get the description for a resource.
|
|
func (i info) description(t action.Type, singleResourceName string) string {
|
|
if s, ok := i.actionDescOverrides[t]; ok {
|
|
return s
|
|
}
|
|
switch t {
|
|
case action.List:
|
|
singleResourceName := strings.TrimPrefix(strings.TrimPrefix(singleResourceName, "an "), "a ")
|
|
return fmt.Sprintf("List %ss", singleResourceName)
|
|
case action.Read:
|
|
return fmt.Sprintf("Read %s", singleResourceName)
|
|
case action.Update:
|
|
return fmt.Sprintf("Update %s", singleResourceName)
|
|
case action.Delete:
|
|
return fmt.Sprintf("Delete %s", singleResourceName)
|
|
case action.Create:
|
|
return fmt.Sprintf("Create %s", singleResourceName)
|
|
}
|
|
switch {
|
|
case strings.HasPrefix(t.String(), "add-"):
|
|
thing := strings.SplitN(t.String(), "-", 2)[1]
|
|
thing = strings.ReplaceAll(thing, "-", " ")
|
|
return fmt.Sprintf("Add %s to %s", thing, singleResourceName)
|
|
case strings.HasPrefix(t.String(), "set-"):
|
|
thing := strings.SplitN(t.String(), "-", 2)[1]
|
|
thing = strings.ReplaceAll(thing, "-", " ")
|
|
return fmt.Sprintf("Set the full set of %s on %s", thing, singleResourceName)
|
|
case strings.HasPrefix(t.String(), "remove-"):
|
|
thing := strings.SplitN(t.String(), "-", 2)[1]
|
|
thing = strings.ReplaceAll(thing, "-", " ")
|
|
return fmt.Sprintf("Remove %s from %s", thing, singleResourceName)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
var resources = map[resource.Type]info{
|
|
resource.Account: {
|
|
scopes: iamScopes,
|
|
actionDescOverrides: map[action.Type]string{
|
|
action.SetPassword: "Set a password on an account, without requiring the current password",
|
|
action.ChangePassword: "Change a password on an account given the current password",
|
|
},
|
|
},
|
|
resource.Alias: {
|
|
scopes: []string{"Global"},
|
|
},
|
|
resource.AuthMethod: {
|
|
scopes: iamScopes,
|
|
actionDescOverrides: map[action.Type]string{
|
|
action.Authenticate: "Authenticate to an auth method",
|
|
},
|
|
},
|
|
resource.AuthToken: {
|
|
scopes: iamScopes,
|
|
},
|
|
resource.Credential: {
|
|
scopes: infraScope,
|
|
},
|
|
resource.CredentialLibrary: {
|
|
scopes: infraScope,
|
|
},
|
|
resource.CredentialStore: {
|
|
scopes: infraScope,
|
|
},
|
|
resource.Group: {
|
|
scopes: append(iamScopes, infraScope...),
|
|
},
|
|
resource.Host: {
|
|
scopes: infraScope,
|
|
},
|
|
resource.HostCatalog: {
|
|
scopes: infraScope,
|
|
},
|
|
resource.HostSet: {
|
|
scopes: infraScope,
|
|
},
|
|
resource.ManagedGroup: {
|
|
scopes: iamScopes,
|
|
},
|
|
resource.Role: {
|
|
scopes: append(iamScopes, infraScope...),
|
|
},
|
|
resource.Scope: {
|
|
scopes: iamScopes,
|
|
},
|
|
resource.Session: {
|
|
scopes: infraScope,
|
|
actionDescOverrides: map[action.Type]string{
|
|
action.Cancel: "Cancel a session",
|
|
action.CancelSelf: "Cancel a session, which must be associated with the calling user",
|
|
action.ReadSelf: "Read a session, which must be associated with the calling user",
|
|
},
|
|
},
|
|
resource.SessionRecording: {
|
|
scopes: iamScopes,
|
|
actionDescOverrides: map[action.Type]string{
|
|
action.Download: "Download a session recording",
|
|
action.ReApplyStoragePolicy: "Reapply the storage policy to a session recording",
|
|
},
|
|
},
|
|
resource.StorageBucket: {
|
|
scopes: iamScopes,
|
|
},
|
|
resource.Target: {
|
|
scopes: infraScope,
|
|
actionDescOverrides: map[action.Type]string{
|
|
action.AuthorizeSession: "Authorize a session via the target",
|
|
},
|
|
},
|
|
resource.User: {
|
|
scopes: iamScopes,
|
|
},
|
|
resource.Worker: {
|
|
scopes: []string{"Global"},
|
|
actionDescOverrides: map[action.Type]string{
|
|
action.CreateControllerLed: "Create a worker using the controller-led workflow",
|
|
action.CreateWorkerLed: "Create a worker using the worker-led workflow",
|
|
},
|
|
},
|
|
}
|