// 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 = "website/content/docs/configuration/identity-access-management/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 := resource.Parent(res); 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=;actions=%s", actionName), } if strings.Contains(actionName, ":") { parentActionName := strings.SplitN(actionName, ":", 1)[0] examples = append([]string{fmt.Sprintf("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/", res.PluralString()), Params: map[string]string{ "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=;actions=%s", a.String()), } if pin != "" { examples = append(examples, fmt.Sprintf("ids=;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 } func (p *Page) MarshalBody() (ret []string) { for _, v := range p.Resources { ret = append(ret, v.Marshal()...) ret = append(ret, "") } return } 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 } func (e *Endpoint) Marshal() (ret []string) { var row []string // Endpoint Field row = append(row, fmt.Sprintf("%s", escape(e.Path))) // Parameters Field pString := "
    " for _, v := range sortedKeys(e.Params) { pString = fmt.Sprintf("%s
  • %s
  • ", pString, v) pString = fmt.Sprintf("%s
    • %s
    ", pString, escape(e.Params[v])) } pString = fmt.Sprintf("%s
", pString) row = append(row, pString) // Actions Field aString := "
    " for _, v := range e.Actions { aString = fmt.Sprintf( "%s
  • %s: %s
  • ", aString, escape(v.Name), v.Description, ) eString := "
      " for _, x := range v.Examples { // Intentionally using markdown code highlighting here for readability eString = fmt.Sprintf("%s
    • `%s`
    • ", eString, x) } eString = fmt.Sprintf("%s
    ", eString) aString = fmt.Sprintf("%s%s", aString, eString) } aString = fmt.Sprintf("%s
", aString) row = append(row, aString) ret = append(ret, fmt.Sprintf("| %s |", strings.Join(row, " | "))) return } 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", }, }, }