diff --git a/docs/config.json b/docs/config.json
index 123d39ccd71..4e5d5e7b30f 100644
--- a/docs/config.json
+++ b/docs/config.json
@@ -1181,6 +1181,10 @@
{
"title": "Architecture",
"slug": "/api/architecture/"
+ },
+ {
+ "title": "How to Build an Access Request Plugin",
+ "slug": "/api/access-plugin/"
}
]
},
diff --git a/docs/img/api/google-sheets.png b/docs/img/api/google-sheets.png
new file mode 100644
index 00000000000..7b36b4e086b
Binary files /dev/null and b/docs/img/api/google-sheets.png differ
diff --git a/docs/pages/api/access-plugin.mdx b/docs/pages/api/access-plugin.mdx
new file mode 100644
index 00000000000..45aec2731ac
--- /dev/null
+++ b/docs/pages/api/access-plugin.mdx
@@ -0,0 +1,767 @@
+---
+title: How to Build an Access Request Plugin
+description: Manage Access Requests using custom workflows with the Teleport API
+---
+
+With Teleport [Access Requests](../access-controls/access-requests.mdx), you can
+assign Teleport users to less privileged roles by default and allow them to
+temporarily escalate their privileges. Reviewers can grant or deny Access
+Requests within your organization's existing communication workflows (e.g.,
+Slack, email, and PagerDuty) using [Access Request
+plugins](../access-controls/access-request-plugins/index.mdx).
+
+You can use Teleport's API client library to build an Access Request plugin that
+integrates with your organization's unique workflows.
+
+In this guide, we will explore a number of Teleport's API client libraries by
+showing you how to write a plugin that lets you manage Access Requests via
+Google Sheets. The plugin lists new Access Requests in a Google Sheets
+spreadsheet, with links to allow or deny each request. You can write the plugin
+in 260 lines of Go code.
+
+![The result of the plugin](../../img/api/google-sheets.png)
+
+
+
+The plugin we will build in this guide is intended as a learning tool. **Do not
+connect it to your production Teleport cluster.** Use a demo cluster instead.
+
+
+
+## Prerequisites
+
+(!docs/pages/includes/commercial-prereqs-tabs.mdx!)
+
+- Go version (=teleport.golang=)+ installed on your workstation. See the [Go
+ download page](https://go.dev/dl/). You do not need to be familiar with Go to
+ complete this guide, though Go knowledge is required if you want to build your
+ own Access Request plugin.
+
+You will need the following in order to set up the demo plugin, which requires
+authenticating to the Google Sheets API:
+
+- A Google Cloud project with permissions to create service accounts.
+- A Google account that you will use to create a Google Sheets spreadsheet. We
+ will grant permissions to edit the spreadsheet to the service account used for
+ the plugin.
+
+
+
+Even if you do not plan to set up the demo project, you can follow this guide to see
+which libraries, types, and functions you can use to develop an Access Request
+plugin.
+
+The demo is a minimal working example, and you can see fully fledged plugins in
+the
+[`gravitational/teleport-plugins`](https://github.com/gravitational/teleport-plugins)
+repository on GitHub.
+
+
+
+## Step 1/7. Set up your Go project
+
+The first step is to create a project directory and initialize a Go module for
+your project:
+
+```code
+$ mkdir teleport-sheets
+$ cd teleport-sheets
+$ go mod init teleport-sheets
+go: creating new go.mod: module teleport-sheets
+```
+
+This creates a new file in your project directory, `go.mod`, which Go's package
+management system uses to fetch your plugin's dependencies.
+
+## Step 2/7. Set up the Google Sheets API
+
+Access Request plugins typically communicate with two APIs. They receive Access
+Request events from the Teleport Auth Service's gRPC API, and use the data to
+interact with the API of your chosen messaging or collaboration tool.
+
+In this section, we will enable the Google Sheets API, create a Google Cloud
+service account for the plugin, and use the service account to authenticate the
+plugin to Google Sheets.
+
+### Enable the Google Sheets API
+
+Enable the Google Sheets API by visiting the following Google Cloud console URL:
+
+https://console.cloud.google.com/apis/enableflow?apiid=sheets.googleapis.com
+
+Ensure that your Google Cloud project is the one you intend to use.
+
+Click **Next** > **Enable**.
+
+### Create a Google Cloud service account for the plugin
+
+Visit the following Google Cloud console URL:
+
+https://console.cloud.google.com/iam-admin/serviceaccounts
+
+Click **Create Service Account**.
+
+For **Service account name**, enter "Teleport Google Sheets Plugin". Google
+Cloud will populate the **Service account ID** field for you.
+
+Click **Create and Continue**. When prompted to grant roles to the service
+account, click **Continue** again. We will create our service account without
+roles. Skip the step to grant users access to the service account, clicking
+**Done**.
+
+The console will take you to the **Service accounts** view. Click the name of
+the service account you just created, then click the **Keys** tab. Click **Add
+Key**, then **Create new key**. Leave the **Key type** as "JSON" and click
+**Create**.
+
+Save your Google Cloud credentials file as `credentials.json` in your Go project
+directory.
+
+Your plugin will use this JSON file to authenticate to Google Sheets.
+
+### Create a Google Sheets spreadsheet
+
+Visit the following URL and make sure you are authenticated as the correct user:
+
+https://sheets.new
+
+Name your spreadsheet.
+
+Give the plugin access to the spreadsheet by clicking **Share**. In the **Add
+people and groups** field, enter
+`teleport-google-sheets-plugin@PROJECT_NAME.iam.gserviceaccount.com`, replacing
+`PROJECT_NAME` with the name of your project. Make sure that the service account
+has "Editor" permissions. Click **Share**, then **Share anyway** when prompted
+with a warning.
+
+By authenticating to Google Sheets with the service account you created, the
+plugin will have access to modify your spreadsheet.
+
+Next, ensure that the following is true within your spreadsheet:
+
+- There is only one sheet
+- The sheet includes the following columns:
+
+|ID|Created|User|Roles|Status|Link|
+|---|---|---|---|---|---|
+
+After we write our Access Request plugin, it will populate the spreadsheet with
+data automatically.
+
+## Step 3/7. Set up Teleport RBAC
+
+In this section, we will set up Teleport roles that enable creating and
+reviewing Access Requests, plus another Teleport role that can generate
+credentials for your Access Request plugin to authenticate to Teleport.
+
+### Create a user and role for the plugin
+
+(!docs/pages/includes/plugins/rbac.mdx!)
+
+### Export the access plugin identity
+
+You will use the `tctl auth sign` command to request the credentials that the
+`access-plugin` needs to connect to your Teleport cluster.
+
+The following `tctl auth sign` command impersonates the `access-plugin` user,
+generates signed credentials, and writes an identity file to the local
+directory:
+
+```code
+$ tctl auth sign --user=access-plugin --out=auth.pem
+```
+
+Teleport's Access Request plugins listen for new and updated Access Requests by
+connecting to the Teleport Auth Service's gRPC endpoint over TLS.
+
+The identity file, `auth.pem`, includes both TLS and SSH credentials. Your
+Access Request plugin uses the SSH credentials to connect to the Proxy Service,
+which establishes a reverse tunnel connection to the Auth Service. The plugin
+uses this reverse tunnel, along with your TLS credentials, to connect to the
+Auth Service's gRPC endpoint.
+
+You will refer to this file later when configuring the plugin.
+
+### Set up Role Access Requests
+
+In this guide, we will use our plugin to manage Role Access Requests. For this
+to work, we will set up Role Access Requests in your cluster.
+
+(!/docs/pages/includes/plugins/editor-request-rbac.mdx!)
+
+## Step 4/7. Write the Access Request plugin
+
+At this point, we have completed the preparatory steps required to run an Access
+Request plugin, including setting up your Teleport roles to support Role Access
+Requests and getting credentials for the APIs the plugin will authenticate to.
+Next, we will write the Access Request plugin.
+
+### Add imports
+
+Create a file called `main.go` in your project directory with the following
+content:
+
+```go
+package main
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/gravitational/teleport-plugins/lib"
+ "github.com/gravitational/teleport-plugins/lib/watcherjob"
+ "github.com/gravitational/teleport/api/client"
+ "github.com/gravitational/teleport/api/types"
+ "github.com/gravitational/trace"
+ "google.golang.org/api/option"
+ sheets "google.golang.org/api/sheets/v4"
+ "google.golang.org/grpc"
+)
+```
+
+Here are the packages our Access Request plugin will import from Go's standard
+library:
+
+|Package|Description|
+|---|---|
+|`context`|Includes the `context.Context` type. `context.Context` is an abstraction for controlling long-running routines, such as connections to external services, that might fail or time out. Programs can cancel contexts or assign them timeouts and metadata. |
+|`errors`|Working with errors.|
+|`fmt`|Formatting data for printing, strings, or errors.|
+|`time`|Dealing with time. We will use this to define a timeout for connecting to the Auth Service.|
+|`strings`|Manipulating strings.|
+
+The pugin imports the following third-party code:
+
+|Package|Description|
+|---|---|
+|`github.com/gravitational/teleport-plugins/lib`|Code shared between Teleport plugins, e.g., to run a background job for retrieving new events from the Teleport Auth Service.|
+|`github.com/gravitational/teleport-plugins/lib/watcherjob`|A library for writing programs that listen for audit events from the Auth Service.|
+|`github.com/gravitational/teleport/api/client`|A library for authenticating to the Auth Service's gRPC API and making requests.|
+|`github.com/gravitational/teleport/api/types`|Types used in the Auth Service API, e.g., Access Requests.|
+|`github.com/gravitational/trace`|Presenting errors with more useful detail than the standard library provides.|
+|`google.golang.org/api/option`|Settings for configuring Google API clients.|
+|`google.golang.org/api/sheets/v4`|The Google Sheets API client library, aliased as `sheets` in our program.|
+|`google.golang.org/grpc`|The gRPC client and server library.|
+
+### Make some initial declarations
+
+After the `import` block, add the following declarations:
+
+```go
+const (
+ proxyAddr string = ""
+ initTimeout = time.Duration(30) * time.Second
+ spreadSheetID string = ""
+)
+
+var requestStates = map[types.RequestState]string{
+ types.RequestState_APPROVED: "APPROVED",
+ types.RequestState_DENIED: "DENIED",
+ types.RequestState_PENDING: "PENDING",
+ types.RequestState_NONE: "NONE",
+}
+
+type googleSheetsPlugin struct {
+ sheetsClient *sheets.SpreadsheetsService
+ teleportClient *client.Client
+}
+```
+
+`proxyAddr` indicates the hostname and port of your Teleport Proxy Service or
+Teleport Enterprise Cloud tenant. Assign it to the address of your own Proxy
+Service, e.g., `mytenant.teleport.sh:443`.
+
+`initTimeout` is the maximum time we will wait to connect to the Auth Service,
+in this case, 30 seconds.
+
+Assign `spreadSheetID` to the ID of the spreadsheet you created earlier. To find
+the spreadsheet ID, visit your spreadsheet in Google Drive. The ID will be in
+the URL path segment called `SPREADSHEET_ID` below:
+
+```text
+https://docs.google.com/spreadsheets/d/SPREADHSEET_ID/edit#gid=0
+```
+
+Access Requests have one of four states: approved, denied, pending, and none.
+Later in the guide, we will need a way to map these states to strings that we
+can write to our Google Sheets spreadsheet. We obtain the request states from
+Teleport's `types` library and map them to strings in the `requestStates` map.
+
+`googleSheetsPlugin` is a struct type that contains the API clients for the
+Teleport Auth Service and Google Sheets. We will explain later why we use a
+struct to organize these two clients, instead of, say, two separate variables.
+
+### Prepare row data
+
+Whether creating a new row of the spreadsheet or updating an existing one, we
+need a way to extract data from an Access Reqeust in order to provide it to
+Google Sheets. Below the `googleSheetsPlugin` type declaration, add the
+following functions:
+
+```go
+func stringPtr(s string) *string { return &s }
+
+func (g *googleSheetsPlugin) makeRowData(ar types.AccessRequest) *sheets.RowData {
+ requestState, ok := requestStates[ar.GetState()]
+
+ // Could not find a state, but this is still a valid Access Request
+ if !ok {
+ requestState = requestStates[types.RequestState_NONE]
+ }
+
+ viewLink := fmt.Sprintf(
+ `=HYPERLINK("%v", "%v")`,
+ "https://"+proxyAddr+"/web/requests/"+ar.GetName(),
+ "View Access Request",
+ )
+
+ return &sheets.RowData{
+ Values: []*sheets.CellData{
+ &sheets.CellData{
+ UserEnteredValue: &sheets.ExtendedValue{
+ StringValue: stringPtr(ar.GetName()),
+ },
+ },
+ &sheets.CellData{
+ UserEnteredValue: &sheets.ExtendedValue{
+ StringValue: stringPtr(ar.GetCreationTime().String()),
+ },
+ },
+ &sheets.CellData{
+ UserEnteredValue: &sheets.ExtendedValue{
+ StringValue: stringPtr(ar.GetUser()),
+ },
+ },
+ &sheets.CellData{
+ UserEnteredValue: &sheets.ExtendedValue{
+ StringValue: stringPtr(strings.Join(ar.GetRoles(), ",")),
+ },
+ },
+ &sheets.CellData{
+ UserEnteredValue: &sheets.ExtendedValue{
+ StringValue: &requestState,
+ },
+ },
+ &sheets.CellData{
+ UserEnteredValue: &sheets.ExtendedValue{
+ FormulaValue: &viewLink,
+ },
+ },
+ },
+ }
+}
+```
+
+The `sheets.RowData` type makes extensive use of pointers to strings, so we
+introduce a utility function called `stringPtr` that returns the pointer to the
+provided string. This makes it easier to assign the values of cells in the
+`sheets.RowData` using chains of function calls.
+
+`makeRowData` is a method of the `googleSheetsPlugin` type. (The `*` before
+`googleSheetsPlugin` indicates that the method receives a *pointer* to a
+`googleSheetsPlugin`.) It takes a `types.AccessRequest`, which Teleport's API
+library uses to represent the fields within an Access Request.
+
+The Google Sheets client library defines a `sheets.RowData` type that we
+include in requests to update a spreadsheet. This function converts a
+`types.AccessRequest` into a `*sheets.RowData` (another pointer).
+
+When extracting the data, we use the `types.AccessRequest.GetName()` method to
+retrieve the ID of the Access Request as a string we can include in the
+spreadsheet.
+
+Users can review an Access Request by visiting a URL within the Teleport Web UI
+that corresponds to the request's ID. `makeRowData` assembles a `=HYPERLINK`
+formula that we can insert into the spreadsheet as a link to this URL.
+
+Next, we'll add two functions that call `makeRowData`, one to create a new row
+and one to update an existing row.
+
+### Create a row
+
+Below `makeRowData`, add the following:
+
+```go
+func (g *googleSheetsPlugin) createRow(ar types.AccessRequest) error {
+ row := g.makeRowData(ar)
+
+ req := sheets.BatchUpdateSpreadsheetRequest{
+ Requests: []*sheets.Request{
+ {
+ AppendCells: &sheets.AppendCellsRequest{
+
+ Fields: "*",
+ Rows: []*sheets.RowData{
+ row,
+ },
+ },
+ },
+ },
+ }
+
+ resp, err := g.sheetsClient.BatchUpdate(spreadSheetID, &req).Do()
+ if err != nil {
+ return trace.Wrap(err)
+ }
+
+ if resp.HTTPStatusCode == 201 || resp.HTTPStatusCode == 200 {
+ fmt.Println("Successfully created a row")
+ } else {
+ fmt.Printf(
+ "Unexpected response code creating a row: %v\n",
+ resp.HTTPStatusCode,
+ )
+ }
+
+ return nil
+
+}
+```
+
+`createRow` submits a request to the Google Sheets API to create a new row based
+on an incoming Access Request, using the data returned by `makeRowData`. It
+returns an error if the attempt to create a row failed.
+
+It assembles a `sheets.BatchUpdateSpreadsheetRequest` and sends it to the Google
+Sheets API using `g.sheetsClient.BatchUpdate()`, returning errors encountered
+while sending the request.
+
+We log unexpected HTTP status codes without returning an error since these may
+be transient server-side issues. A production Access Request plugin would handle
+these situations in a more sophisticated way, e.g., storing the request so it
+can retry it later.
+
+### Update a row
+
+The code for updating a row is similar to the code for creating a new row. Add
+this function below `createRow`:
+
+```go
+func (g *googleSheetsPlugin) updateRow(ar types.AccessRequest, rowNum int64) error {
+ row := g.makeRowData(ar)
+
+ req := sheets.BatchUpdateSpreadsheetRequest{
+ Requests: []*sheets.Request{
+ {
+ UpdateCells: &sheets.UpdateCellsRequest{
+
+ Fields: "*",
+ Start: &sheets.GridCoordinate{
+ RowIndex: rowNum,
+ },
+ Rows: []*sheets.RowData{
+ row,
+ },
+ },
+ },
+ },
+ }
+
+ resp, err := g.sheetsClient.BatchUpdate(spreadSheetID, &req).Do()
+ if err != nil {
+ return trace.Wrap(err)
+ }
+
+ if resp.HTTPStatusCode == 201 || resp.HTTPStatusCode == 200 {
+ fmt.Println("Successfully updated a row")
+ } else {
+ fmt.Printf(
+ "Unexpected response code updating a row: %v\n",
+ resp.HTTPStatusCode,
+ )
+ }
+
+ return nil
+
+}
+```
+
+The only difference between `updateRow` and `createRow` is that we send a
+`&sheets.UpdateCellsRequest` instead of a `&sheets.AppendCellsRequest`. This
+function takes the number of a row within the spreadsheet to update and sends a
+request to update that row with information from the provided Access Request.
+
+### Determine where to update the spreadsheet
+
+Below `updateRow`, add this function:
+
+```go
+func (g *googleSheetsPlugin) updateSpreadsheet(ar types.AccessRequest) error {
+ s, err := g.sheetsClient.Get(spreadSheetID).IncludeGridData(true).Do()
+ if err != nil {
+ return trace.Wrap(err)
+ }
+
+ if len(s.Sheets) != 1 {
+ return trace.Wrap(
+ errors.New("the spreadsheet must have a single sheet"),
+ )
+ }
+
+ for _, d := range s.Sheets[0].Data {
+ for i, r := range d.RowData {
+ if r.Values[0] != nil &&
+ r.Values[0].UserEnteredValue != nil &&
+ r.Values[0].UserEnteredValue.StringValue != nil &&
+ *r.Values[0].UserEnteredValue.StringValue == ar.GetName() {
+ if err := g.updateRow(ar, int64(i)); err != nil {
+ return trace.Wrap(err)
+ }
+ fmt.Println("Updated a spreadsheet row.")
+ }
+ }
+ }
+ return nil
+}
+```
+
+`updateSpreadSheet` takes a `types.AccessRequest`, gets the latest data from
+your spreadsheet, determines which row to update, and calls `updateRow`
+accordingly. It uses linear search to look up the first column within each row
+of the sheet and check whether that column matches the ID of the Access Request.
+It then calls `updateRow` with the Access Request and the row's number.
+
+### Handle incoming Access Requests
+
+Next, we will determine whether to call `updateSpreadsheet` or `createRow`
+depending on whether an incoming Access Request event is a new Access Request or
+an update to an existing one. Add the following function below
+`updateSpreadsheet`:
+
+```go
+func (g *googleSheetsPlugin) handleEvent(ctx context.Context, event types.Event) error {
+
+ if event.Resource == nil {
+ return nil
+ }
+
+ r := event.Resource.(types.AccessRequest)
+
+ if r.GetState() == types.RequestState_PENDING {
+ return g.createRow(r)
+ }
+
+ return g.updateSpreadsheet(r)
+}
+```
+
+`handleEvent` checks whether an Access Request is in a pending state, i.e.,
+whether the request is new. If so, we call `createRow`. If not, we call
+`updateSpreadsheet`.
+
+It is worth noting that the function takes any `types.Event`, which includes
+Access Requests but also other audit events, such as the creation of a
+certificate. We will explain in the next section why we are using this function
+signature for `handleEvent`.
+
+### Run a watcher job
+
+Now that we have a function that can handle incoming Teleport events, we will
+add a function that calls our handler function when it receives an event. Add
+the following below `handleEvent`:
+
+```go
+func (g *googleSheetsPlugin) run() error {
+ ctx := context.Background()
+ proc := lib.NewProcess(ctx)
+ watcherJob := watcherjob.NewJob(
+ g.teleportClient,
+ watcherjob.Config{
+ Watch: types.Watch{Kinds: []types.WatchKind{types.WatchKind{Kind: types.KindAccessRequest}}},
+ },
+ g.handleEvent,
+ )
+
+ proc.SpawnCriticalJob(watcherJob)
+
+ fmt.Println("Started the watcher job")
+
+ <-watcherJob.Done()
+
+ fmt.Println("The watcher job is finished")
+
+ return nil
+}
+```
+
+The `run` function starts a new background routine using `lib.NewProcess(ctx)`.
+This returns an implementation of the `lib.Process` interface, which Access
+Request plugins use to run jobs independently of the main program. This function
+takes a Go *context*, an abstraction that Go libraries often use to control
+long-running routines.
+
+Next, `run` calls `watcherjob.NewJob`. A watcher job is an implementation of a
+`lib.ServiceJob`, a task for the `lib.Process` to execute. In this case, the
+`lib.ServiceJob` listens for new events from the Teleport Auth Service and
+executes code in response.
+
+In `watcherjob.NewJob`, we have configured the watcher job to use a Teleport
+client (which we will create in the next section), as well as to watch only for
+Access Request events.
+
+Finally, by passing the `g.handleEvent` method to `watcherjob.NewJob`, we
+instruct the watcher job to call `g.handleEvent` whenever it receives an Access
+Request. The function we pass to `watcherjob.NewJob` must have the following
+signature, which we implement in the `handleEvent` method:
+
+```go
+func (ctx context.Context, event types.Event) error
+```
+
+This is also why we define `handleEvent` as a method of `googleSheetsPlugin`,
+since it means that we can include data about our API clients in this function
+while adhering to the expected function signature (and without declaring more
+global variables).
+
+Finally, we begin the watcher job within our `lib.Process` using
+`proc.SpawnCriticalJob`, and wait for the job to terminate. We do this by
+receiving from a Go *channel*, which is a way for multiple concurrent routines
+to communicate with one another.
+
+### Initialize the API clients
+
+Now we have all the code we need to use the Teleport and Google Sheets API
+clients to listen for Access Request events and use them to maintain a
+spreadsheet. The final step is to start our program by initializing the API
+clients.
+
+Add the following below the `run` function we defined in the last section:
+
+```go
+func main() {
+ ctx := context.Background()
+ svc, err := sheets.NewService(ctx, option.WithCredentialsFile("credentials.json"))
+ if err != nil {
+ panic(err)
+ }
+
+ ctx, cancel := context.WithTimeout(ctx, initTimeout)
+ defer cancel()
+
+ creds := client.LoadIdentityFile("auth.pem")
+
+ teleport, err := client.New(ctx, client.Config{
+ Addrs: []string{proxyAddr},
+ Credentials: []client.Credentials{creds},
+ DialOpts: []grpc.DialOption{
+ grpc.WithReturnConnectionError(),
+ },
+ })
+ if err != nil {
+ panic(err)
+ }
+
+ gs := googleSheetsPlugin{
+ sheetsClient: sheets.NewSpreadsheetsService(svc),
+ teleportClient: teleport,
+ }
+
+ if err := gs.run(); err != nil {
+ panic(err)
+ }
+}
+```
+
+The `main` function, the entrypoint to our program, initializes a
+`googleSheetsPlugin` and uses it run the plugin.
+
+The function creates a Google Sheets API client by loading the credentials file
+you downloaded earlier at the relative path `credentials.json`.
+
+`client` is Teleport's library for setting up an API client. Our plugin does so
+by calling `client.LoadIdentityFile` to obtain a `client.Credentials`. It then
+uses the `client.Credentials` to call `client.New`, which connects to the
+Teleport Proxy Service specified in the `Addrs` field using the provided
+identity file.
+
+In this example, we are passing the `grpc.WithReturnConnectionError()` function
+call to `client.New`, which instructs the gRPC client to return more detailed
+connection errors.
+
+
+
+This program does not validate your credentials or Teleport cluster address.
+Make sure that:
+
+- The identity file you exported earlier does not have an expired TTL
+- The value you supplied for the `proxyAddr` constant includes both the host
+ **and** the web port of your Teleport Proxy Service, e.g.,
+ `mytenant.teleport.sh:443`
+
+
+
+## Step 7/7. Test your plugin
+
+Now that you have written your plugin, run it to forward Access Requests from
+your Teleport cluster to Google Sheets. Execute the following commands from
+within your project directory:
+
+```code
+$ go get
+$ go run main.go
+```
+
+Now that the plugin is running, create an Access Request:
+
+(!docs/pages/includes/plugins/create-request.mdx!)
+
+You should see the new Access Request in your spreadsheet with the `PENDING`
+state.
+
+In your spreadsheet, click "View Access Request" next to your new request. Sign
+into the Teleport Web UI as your original user. When you submit your review,
+e.g., deny the request, the new status will appear within the spreadsheet.
+
+
+
+Access Request plugins must not enable reviewing Access Requests via the plugin,
+and must always refer a reviewer to the Teleport Web UI to complete the review.
+Otherwise, an unauthorized party could spoof traffic to the plugin and escalate
+privileges.
+
+
+
+## Next steps
+
+In this guide, we showed you how to set up an Access Request plugin using
+Teleport's API client libraries. To go beyond the minimal plugin we demonstrate
+in this guide, you can use the Teleport API to set up more sophisticated
+workflows that take full advantage of your communication and project management
+tools.
+
+### Use the common plugin base
+
+The
+[`common.BaseApp`](https://pkg.go.dev/github.com/gravitational/teleport-plugins/access/common#BaseApp)
+type makes it easier to write an Access Request plugin based on a messaging
+application like Discord or Slack, helping to ensure that your plugin implements
+a full set of features.
+
+### Manage state
+
+While the plugin we developed in this guide is stateless, updating Access
+Request information by searching all rows of a spreadsheet, real-world Access
+Request plugins typically need to manage state. You can use the
+[`plugindata`](https://pkg.go.dev/github.com/gravitational/teleport-plugins/lib/plugindata)
+package to make it easier for your Access Request plugin to do this.
+
+### Consult the examples
+
+Explore the
+[`gravitational/teleport-plugins`](https://github.com/gravitational/teleport-plugins)
+repository on GitHub for examples of plugins developed at Teleport. You can see
+how these plugins use the packages we discuss in this guide, as well as how they
+add more complete functionality like configuraiton validation and state
+management.
+
+### Provision the plugin with short-lived credentials
+
+In this example, we used the `tctl auth sign` command to fetch credentials for
+the plugin. For production usage, we recommend provisioning short-lived
+credentials via Machine ID, which reduces the risk of these credentials becoming
+stolen. View our [Machine ID documentation](../machine-id/introduction.mdx) to
+learn more.
+
diff --git a/docs/pages/api/introduction.mdx b/docs/pages/api/introduction.mdx
index 5e412048687..a4e06c4da00 100644
--- a/docs/pages/api/introduction.mdx
+++ b/docs/pages/api/introduction.mdx
@@ -22,3 +22,6 @@ Here is what you can do with the Go Client:
Create an API client in 3 minutes with the [Getting Started
Guide](./getting-started.mdx).
+
+Follow our [Access Request Plugin Guide](./access-plugin.mdx) for a tour of
+Teleport's API libraries and develop a minimal working example.
diff --git a/docs/pages/includes/plugins/editor-request-rbac.mdx b/docs/pages/includes/plugins/editor-request-rbac.mdx
index 00266568823..a4ee41b8de9 100644
--- a/docs/pages/includes/plugins/editor-request-rbac.mdx
+++ b/docs/pages/includes/plugins/editor-request-rbac.mdx
@@ -36,32 +36,9 @@ role 'editor-requester' has been created
```
Allow yourself to review requests by users with the `editor-requester` role by
-assigning yourself the `editor-reviewer` role. First, retrieve your user
-definition:
+assigning yourself the `editor-reviewer` role.
-```code
-$ TELEPORT_USER=$(tsh status --format=json | jq -r .active.username)
-$ tctl get user/${TELEPORT_USER?} > user.yaml
-```
-
-Edit `user.yaml` to add the `editor-reviewer` role:
-
-```diff
- spec:
- roles:
- - access
- - editor
-+ - editor-reviewer
-```
-
-Update your user definition:
-
-```code
-$ tctl create -f user.yaml
-```
-
-Log out of Teleport and log in again. You will now have the ability to review
-requests for the `editor` role.
+(!docs/pages/includes/add-role-to-user.mdx role="editor-reviewer"!)
Create a user called `myuser` who has the `editor-requester` role. This user
cannot edit your cluster configuration unless they request the `editor` role: