mirror of
https://github.com/gravitational/teleport
synced 2024-10-19 16:53:57 +00:00
Add a guide to writing an Access Request plugin (#22441)
* Add a guide to writing an Access Request plugin Closes #22227 This guide uses a minimal working example of an Access Request plugin to demonstrate some of the key libraries that Teleport makes available to plugin developers. The guide is based on a 270-line plugin that manages Access Requests using Google Sheets. * Respond to hugoShaka feedback * Respond to zmb3 feedback * Add some minor tweaks - Update the line of code count in the demo program - Mention Machine ID * Respond to alexfornuto PR feedback
This commit is contained in:
parent
3d60c64067
commit
c553fa7d66
|
@ -1181,6 +1181,10 @@
|
|||
{
|
||||
"title": "Architecture",
|
||||
"slug": "/api/architecture/"
|
||||
},
|
||||
{
|
||||
"title": "How to Build an Access Request Plugin",
|
||||
"slug": "/api/access-plugin/"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
BIN
docs/img/api/google-sheets.png
Normal file
BIN
docs/img/api/google-sheets.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 298 KiB |
767
docs/pages/api/access-plugin.mdx
Normal file
767
docs/pages/api/access-plugin.mdx
Normal file
|
@ -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)
|
||||
|
||||
<Notice type="danger">
|
||||
|
||||
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.
|
||||
|
||||
</Notice>
|
||||
|
||||
## 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.
|
||||
|
||||
<Admonition type="tip">
|
||||
|
||||
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.
|
||||
|
||||
</Admonition>
|
||||
|
||||
## 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.
|
||||
|
||||
<Admonition type="warning">
|
||||
|
||||
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`
|
||||
|
||||
</Admonition>
|
||||
|
||||
## 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.
|
||||
|
||||
<Notice type="danger">
|
||||
|
||||
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.
|
||||
|
||||
</Notice>
|
||||
|
||||
## 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.
|
||||
|
|
@ -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.
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in a new issue