Simplifying Zero Trust Security for AWS with Teleport
Jan 23
Virtual
Register Now
Teleport logoTry For Free
Fork me on GitHub

Teleport

How to Build an Access Request Plugin

With Teleport Access Requests, 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.

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.

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

  • A running Teleport cluster. If you want to get started with Teleport, sign up for a free trial.

  • The tctl admin tool and tsh client tool.

    Visit Installation for instructions on downloading tctl and tsh.

  • Go version 1.22.10+ installed on your workstation. See the Go download page. 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.
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. repository on GitHub.

Step 1/5. Set up your Go project

Download the source code for our minimal Access Request plugin:

git clone https://github.com/gravitational/teleport -b branch/v16
cd teleport/examples/access-plugin-minimal

For the rest of this guide, we will show you how to set up this plugin and explore the way the plugin uses Teleport's API to integrate Access Requests with a particular workflow.

Step 2/5. 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:
IDCreatedUserRolesStatusLink

After we write our Access Request plugin, it will populate the spreadsheet with data automatically.

Step 3/5. 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

Teleport's Access Request plugins authenticate to your Teleport cluster as a user with permissions to list and read Access Requests. This way, plugins can retrieve Access Requests from the Teleport Auth Service and present them to reviewers.

Define a user and role called access-plugin by adding the following content to a file called access-plugin.yaml:

kind: role
version: v5
metadata:
  name: access-plugin
spec:
  allow:
    rules:
      - resources: ['access_request']
        verbs: ['list', 'read']
      - resources: ['access_plugin_data']
        verbs: ['update']
---
kind: user
metadata:
  name: access-plugin
spec:
  roles: ['access-plugin']
version: v2

Create the user and role:

tctl create -f access-plugin.yaml

As with all Teleport users, the Teleport Auth Service authenticates the access-plugin user by issuing short-lived TLS credentials. In this case, we will need to request the credentials manually by impersonating the access-plugin role and user.

If you are running a self-hosted Teleport Enterprise deployment and are using tctl from the Auth Service host, you will already have impersonation privileges.

To grant your user impersonation privileges for access-plugin, define a role called access-plugin-impersonator by pasting the following YAML document into a file called access-plugin-impersonator.yaml:

kind: role
version: v7
metadata:
  name: access-plugin-impersonator
spec:
  allow:
    impersonate:
      roles:
      - access-plugin
      users:
      - access-plugin

Create the access-plugin-impersonator role:

tctl create -f access-plugin-impersonator.yaml

If you are providing identity files to the plugin with Machine ID, assign the access-plugin role to the Machine ID bot user. Otherwise, assign this role to the user you plan to use to generate credentials for the access-plugin role and user:

Assign the access-plugin-impersonator role to your Teleport user by running the appropriate commands for your authentication provider:

  1. Retrieve your local user's roles as a comma-separated list:

    ROLES=$(tsh status -f json | jq -r '.active.roles | join(",")')
  2. Edit your local user to add the new role:

    tctl users update $(tsh status -f json | jq -r '.active.username') \ --set-roles "${ROLES?},access-plugin-impersonator"
  3. Sign out of the Teleport cluster and sign in again to assume the new role.

  1. Open your github authentication connector in a text editor:

    tctl edit github/github
  2. Edit the github connector, adding access-plugin-impersonator to the teams_to_roles section.

    The team you should map to this role depends on how you have designed your organization's role-based access controls (RBAC). However, the team must include your user account and should be the smallest team possible within your organization.

    Here is an example:

      teams_to_roles:
        - organization: octocats
          team: admins
          roles:
            - access
    +       - access-plugin-impersonator
    
  3. Apply your changes by saving closing the file in your editor.

  4. Sign out of the Teleport cluster and sign in again to assume the new role.

  1. Retrieve your saml configuration resource:

    tctl get --with-secrets saml/mysaml > saml.yaml

    Note that the --with-secrets flag adds the value of spec.signing_key_pair.private_key to the saml.yaml file. Because this key contains a sensitive value, you should remove the saml.yaml file immediately after updating the resource.

  2. Edit saml.yaml, adding access-plugin-impersonator to the attributes_to_roles section.

    The attribute you should map to this role depends on how you have designed your organization's role-based access controls (RBAC). However, the group must include your user account and should be the smallest group possible within your organization.

    Here is an example:

      attributes_to_roles:
        - name: "groups"
          value: "my-group"
          roles:
            - access
    +       - access-plugin-impersonator
    
  3. Apply your changes:

    tctl create -f saml.yaml
  4. Sign out of the Teleport cluster and sign in again to assume the new role.

  1. Retrieve your oidc configuration resource:

    tctl get oidc/myoidc --with-secrets > oidc.yaml

    Note that the --with-secrets flag adds the value of spec.signing_key_pair.private_key to the oidc.yaml file. Because this key contains a sensitive value, you should remove the oidc.yaml file immediately after updating the resource.

  2. Edit oidc.yaml, adding access-plugin-impersonator to the claims_to_roles section.

    The claim you should map to this role depends on how you have designed your organization's role-based access controls (RBAC). However, the group must include your user account and should be the smallest group possible within your organization.

    Here is an example:

      claims_to_roles:
        - name: "groups"
          value: "my-group"
          roles:
            - access
    +       - access-plugin-impersonator
    
  3. Apply your changes:

    tctl create -f oidc.yaml
  4. Sign out of the Teleport cluster and sign in again to assume the new role.

You will now be able to generate signed certificates for the access-plugin role and user.

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:

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.

For the purpose of this guide, we will define an editor-requester role, which can request the built-in editor role, and an editor-reviewer role that can review requests for the editor role.

Create a file called editor-request-rbac.yaml with the following content:

kind: role
version: v7
metadata:
  name: editor-reviewer
spec:
  allow:
    review_requests:
      roles: ['editor']
---
kind: role
version: v7
metadata:
  name: editor-requester
spec:
  allow:
    request:
      roles: ['editor']
      thresholds:
        - approve: 1
          deny: 1

Create the roles you defined:

tctl create -f editor-request-rbac.yaml
role 'editor-reviewer' has been createdrole 'editor-requester' has been created

Allow yourself to review requests by users with the editor-requester role by assigning yourself the editor-reviewer role.

Assign the editor-reviewer role to your Teleport user by running the appropriate commands for your authentication provider:

  1. Retrieve your local user's roles as a comma-separated list:

    ROLES=$(tsh status -f json | jq -r '.active.roles | join(",")')
  2. Edit your local user to add the new role:

    tctl users update $(tsh status -f json | jq -r '.active.username') \ --set-roles "${ROLES?},editor-reviewer"
  3. Sign out of the Teleport cluster and sign in again to assume the new role.

  1. Open your github authentication connector in a text editor:

    tctl edit github/github
  2. Edit the github connector, adding editor-reviewer to the teams_to_roles section.

    The team you should map to this role depends on how you have designed your organization's role-based access controls (RBAC). However, the team must include your user account and should be the smallest team possible within your organization.

    Here is an example:

      teams_to_roles:
        - organization: octocats
          team: admins
          roles:
            - access
    +       - editor-reviewer
    
  3. Apply your changes by saving closing the file in your editor.

  4. Sign out of the Teleport cluster and sign in again to assume the new role.

  1. Retrieve your saml configuration resource:

    tctl get --with-secrets saml/mysaml > saml.yaml

    Note that the --with-secrets flag adds the value of spec.signing_key_pair.private_key to the saml.yaml file. Because this key contains a sensitive value, you should remove the saml.yaml file immediately after updating the resource.

  2. Edit saml.yaml, adding editor-reviewer to the attributes_to_roles section.

    The attribute you should map to this role depends on how you have designed your organization's role-based access controls (RBAC). However, the group must include your user account and should be the smallest group possible within your organization.

    Here is an example:

      attributes_to_roles:
        - name: "groups"
          value: "my-group"
          roles:
            - access
    +       - editor-reviewer
    
  3. Apply your changes:

    tctl create -f saml.yaml
  4. Sign out of the Teleport cluster and sign in again to assume the new role.

  1. Retrieve your oidc configuration resource:

    tctl get oidc/myoidc --with-secrets > oidc.yaml

    Note that the --with-secrets flag adds the value of spec.signing_key_pair.private_key to the oidc.yaml file. Because this key contains a sensitive value, you should remove the oidc.yaml file immediately after updating the resource.

  2. Edit oidc.yaml, adding editor-reviewer to the claims_to_roles section.

    The claim you should map to this role depends on how you have designed your organization's role-based access controls (RBAC). However, the group must include your user account and should be the smallest group possible within your organization.

    Here is an example:

      claims_to_roles:
        - name: "groups"
          value: "my-group"
          roles:
            - access
    +       - editor-reviewer
    
  3. Apply your changes:

    tctl create -f oidc.yaml
  4. Sign out of the Teleport cluster and sign in again to assume the new role.

Create a user called myuser who has the editor-requester role. This user cannot edit your cluster configuration unless they request the editor role:

tctl users add myuser --roles=editor-requester

tctl will print an invitation URL to your terminal. Visit the URL and log in as myuser for the first time, registering credentials as configured for your Teleport cluster.

Later in this guide, you will have myuser request the editor role so you can review the request using the Teleport plugin.

Step 4/5. Write the Access Request plugin

In this step, we will walk you through the structure of the Access Request plugin in examples/access-plugin-minimal/main.go. You can use the example here to write your own Access Request plugin.

Imports

Here are the packages our Access Request plugin will import from Go's standard library:

PackageDescription
contextIncludes 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.
errorsWorking with errors.
fmtFormatting data for printing, strings, or errors.
stringsManipulating strings.

The plugin imports the following third-party code:

PackageDescription
github.com/gravitational/teleport/api/clientA library for authenticating to the Auth Service's gRPC API and making requests.
github.com/gravitational/teleport/api/typesTypes used in the Auth Service API, e.g., Access Requests.
github.com/gravitational/tracePresenting errors with more useful detail than the standard library provides.
google.golang.org/api/optionSettings for configuring Google API clients.
google.golang.org/api/sheets/v4The Google Sheets API client library, aliased as sheets in our program.
google.golang.org/grpcThe gRPC client and server library.

Configuration

First, we declare two constants that you need to configure for your environment:

// Copyright 2023 Gravitational, Inc
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

const (
	proxyAddr     string = "CHANGE ME"
	spreadSheetID string = "CHANGE ME"
)

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.

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:

https://docs.google.com/spreadsheets/d/SPREADHSEET_ID/edit#gid=0

The AccessRequestPlugin type

The plugin.go file declares types that we will use to organize our Access Request plugin code:

// Copyright 2023 Gravitational, Inc
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
	"context"

	sheets "google.golang.org/api/sheets/v4"

	"github.com/gravitational/teleport/api/client"
	"github.com/gravitational/teleport/api/types"
)

type AccessRequestPlugin struct {
	TeleportClient *client.Client
	EventHandler   interface {
		HandleEvent(ctx context.Context, event types.Event) error
	}
}

type googleSheetsClient struct {
	sheetsClient *sheets.SpreadsheetsService
}

The AccessRequestPlugin type represents a generic Access Request plugin, and you can use this type to build your own plugin. It contains a Teleport API client and an EventHandler, any Go type that implements a HandleEvent method.

In our case, the type that implements HandleEvent is googleSheetsClient, a struct type that contains an API client for Google Sheets.

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 Request in order to provide it to Google Sheets. We achieve this with the makeRowData method:

// Copyright 2023 Gravitational, Inc
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
	"fmt"
	"strings"

	sheets "google.golang.org/api/sheets/v4"

	"github.com/gravitational/teleport/api/types"
)

func stringPtr(s string) *string { return &s }

var requestStates = map[types.RequestState]string{
	types.RequestState_APPROVED: "APPROVED",
	types.RequestState_DENIED:   "DENIED",
	types.RequestState_PENDING:  "PENDING",
	types.RequestState_NONE:     "NONE",
}

func (g *googleSheetsClient) makeRowData(ar types.AccessRequest) *sheets.RowData {
	requestState := requestStates[ar.GetState()]

	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 googleSheetsClient type. (The * before googleSheetsClient indicates that the method receives a pointer to a googleSheetsClient.) 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).

Access Requests have one of four states: approved, denied, pending, and none. We obtain the request states from Teleport's types library and map them to strings in the requestStates map.

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.

Create a row

The following function 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:

// Copyright 2023 Gravitational, Inc
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
	"github.com/gravitational/trace"
	sheets "google.golang.org/api/sheets/v4"

	"github.com/gravitational/teleport/api/types"
)

func (g *googleSheetsClient) 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 {
		return nil
	}

	return trace.Errorf("Unexpected response code creating a row: %v",
		resp.HTTPStatusCode)
}

createRow 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:

// Copyright 2023 Gravitational, Inc
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
	"fmt"

	"github.com/gravitational/trace"
	sheets "google.golang.org/api/sheets/v4"

	"github.com/gravitational/teleport/api/types"
)

func (g *googleSheetsClient) 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 {
		return nil
	}
	return trace.Wrap(
		fmt.Errorf(
			"Unexpected response code updating a row: %v\n",
			resp.HTTPStatusCode),
	)
}

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

When our program receives an event that updates an Access Request, it needs a way to look up the row in the spreadsheet that corresponds to the Access Request so it can update the row:

// Copyright 2023 Gravitational, Inc
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
	"errors"

	"github.com/gravitational/trace"

	"github.com/gravitational/teleport/api/types"
)

func (g *googleSheetsClient) 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)
				}
			}
		}
	}
	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

The plugin calls a handler function when it receives an event. To set this up, we use the Run method of our generic AccessRequestPlugin type, which contains the main loop of the plugin:

// Copyright 2023 Gravitational, Inc
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
	"context"
	"fmt"

	"github.com/gravitational/trace"

	"github.com/gravitational/teleport/api/types"
)

func (g *googleSheetsClient) HandleEvent(ctx context.Context, event types.Event) error {
	if event.Resource == nil {
		return nil
	}

	if _, ok := event.Resource.(*types.WatchStatusV1); ok {
		fmt.Println("Successfully started listening for Access Requests...")
		return nil
	}

	r, ok := event.Resource.(types.AccessRequest)
	if !ok {
		fmt.Printf("Unknown (%T) event received, skipping.\n", event.Resource)
		return nil
	}

	if r.GetState() == types.RequestState_PENDING {
		if err := g.createRow(r); err != nil {
			return err
		}
		fmt.Println("Successfully created a row")
		return nil
	}

	if err := g.updateSpreadsheet(r); err != nil {
		return err
	}
	fmt.Println("Successfully updated a spreadsheet row")
	return nil
}

func (p *AccessRequestPlugin) Run() error {
	ctx := context.Background()

	watch, err := p.TeleportClient.NewWatcher(ctx, types.Watch{
		Kinds: []types.WatchKind{
			types.WatchKind{Kind: types.KindAccessRequest},
		},
	})

	if err != nil {
		return trace.Wrap(err)
	}
	defer watch.Close()

	fmt.Println("Starting the watcher job")

	for {
		select {
		case e := <-watch.Events():
			if err := p.EventHandler.HandleEvent(ctx, e); err != nil {
				return trace.Wrap(err)
			}
		case <-watch.Done():
			fmt.Println("The watcher job is finished")
			return nil
		}
	}
}

As we described above, the AccessRequestPlugin type's EventHandler field is assigned to an interface with a HandleEvent method. In this case, the implementation is *googleSheetsClient.HandleEvent. This method checks whether an Access Request is in a pending state, i.e., whether the request is new. If so, it calls createRow. If not, it calls updateSpreadsheet.

The Teleport API client type, client.Client, has a NewWatcher method that listens for new audit events from the Auth Service API via a gRPC stream. The second parameter of the method indicates the type of audit event to watch for, in this case, events having to do with Teleport Access Requests.

The result of NewWatcher, a types.Watcher, enables Run to respond to new audit events by calling the Events method. This returns a Go channel, a runtime abstraction that allows concurrent routines to communicate. Another channel, returned by Done, indicates when the watcher has finished.

In a for loop, the Run method receives from either the Done channel or Events channel, whichever is ready to send first. If it receives from the Events channel, it calls the HandleEvent method to process the event.

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:

// Copyright 2023 Gravitational, Inc
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
	"context"

	"google.golang.org/api/option"
	sheets "google.golang.org/api/sheets/v4"
	"google.golang.org/grpc"

	"github.com/gravitational/teleport/api/client"
)

func main() {
	ctx := context.Background()
	svc, err := sheets.NewService(ctx, option.WithCredentialsFile("credentials.json"))
	if err != nil {
		panic(err)
	}

	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)
	}
	defer teleport.Close()

	gs := googleSheetsClient{
		sheetsClient: sheets.NewSpreadsheetsService(svc),
	}

	plugin := AccessRequestPlugin{
		TeleportClient: teleport,
		EventHandler:   &gs,
	}

	if err := plugin.Run(); err != nil {
		panic(err)
	}
}

The main function, the entrypoint to our program, initializes an AccessRequestPlugin and googleSheetsClient and uses them 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.

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

Step 5/5. Test your plugin

Run the plugin to forward Access Requests from your Teleport cluster to Google Sheets. Execute the following command from within examples/access-plugin-minimal:

go run teleport-sheets

Now that the plugin is running, create an Access Request:

A Teleport admin can create an Access Request for another user with tctl:

tctl request create myuser --roles=editor

Users can use tsh to create an Access Request and log in with approved roles:

tsh request create --roles=editor
Seeking request approval... (id: 8f77d2d1-2bbf-4031-a300-58926237a807)

Users can request access using the Web UI by visiting the "Access Requests" tab and clicking "New Request":

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.

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 package to make it easier for your Access Request plugin to do this.

Consult the examples

Explore the gravitational/teleport. 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 configuration 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 to learn more.