From 981ccb982c1e2b09ff98f370019748d783e050d5 Mon Sep 17 00:00:00 2001 From: Logan Davis <38335829+logand22@users.noreply.github.com> Date: Thu, 31 Mar 2022 09:19:01 -0500 Subject: [PATCH 01/18] Update teleport-plugin docs with OCI information (#473) --- README.md | 4 + access/README.md | 6 + access/email/README.md | 65 +++++++---- access/gitlab/README.md | 4 +- access/jira/README.md | 69 ++++++++---- access/mattermost/README.md | 186 ++++++++++++++++--------------- access/pagerduty/README.md | 166 +++++++++++++-------------- access/slack/README.md | 156 ++++++++++++++++++++++++-- access/slack/example_config.toml | 2 - event-handler/README.md | 81 ++++++++++---- 10 files changed, 485 insertions(+), 254 deletions(-) diff --git a/README.md b/README.md index 3ad680912..f8f99ad47 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,10 @@ sendind webhooks when a new request is created, or a request state is changed, and it allows optionally listening for the 3rd party app callbacks to facilitate the approval workdlow. See more in the [access/webhooks/README.md](/access/webhooks/README.md) +## Event Handler + +The [Teleport Event Handler Plugin](./event-handler) is used to export audit log events to a fluentd service. For more information, visit the Fluentd setup guide at [goteleport.com](https://goteleport.com/docs/setup/guides/fluentd/) or checkout the [README](./event-handler/README.md). + ## Terraform Provider The [Teleport Terraform Provider](./terraform) makes it easy to create resources using diff --git a/access/README.md b/access/README.md index fe68c92d9..5f4fb5db6 100644 --- a/access/README.md +++ b/access/README.md @@ -1,3 +1,9 @@ +# Access Plugins + +The various plugins within this directory allow teleport users the ability to intergrate access request notifications and approval workflows with third party technologies. They also serve as examples for building your own integration. For more information on the plugins available visit the `README.md` within each plugins respective directory. + +For more information on Access Requests with Teleport, check out this [blog post](https://goteleport.com/blog/access-requests/) + ### Access API The Teleport Access API has been moved into the main Teleport repo, and can be imported from `github.com/gravitational/teleport/api`. To see examples of how to get started with the Teleport API, take a look at our [go-client example](https://github.com/gravitational/teleport/tree/master/examples/go-client) or read the [API docs](https://goteleport.com/docs/api/introduction/). diff --git a/access/email/README.md b/access/email/README.md index 9eec84934..bf2dc5010 100644 --- a/access/email/README.md +++ b/access/email/README.md @@ -2,11 +2,17 @@ The plugin allows teams to receive email notifications about new access requests. -## Setup +## Install the plugin -### Install the plugin +There are several methods to installing and using the Teleport Email Plugin: -Get the plugin distribution. +1. Use a [precompiled binary](#precompiled-binary) + +2. Use a [docker image](#docker-image) + +3. Install from [source](#building-from-source) + +### Precompiled Binary ```bash $ curl -L https://get.gravitational.com/teleport-access-email-v7.1.0-linux-amd64-bin.tar.gz @@ -15,11 +21,34 @@ $ cd teleport-access-email $ ./install ``` -### Teleport User and Role +### Docker Image +```bash +$ docker pull quay.io/gravitational/access-plugin-email:9.0.2 +``` + +```bash +$ docker run quay.io/gravitational/access-plugin-email:9.0.2 version +teleport-email v9.0.2 git:teleport-email-v9.0.2-0-g9e149895 go1.17.8 +``` + +For a list of available tags, visit [https://quay.io/](https://quay.io/repository/gravitational/access-plugin-email?tab=tags) + +### Building from source + +To build the plugin from source you need [Go](https://go.dev/) and `make`. + +```bash +$ git clone https://github.com/gravitational/teleport-plugins.git +$ cd teleport-plugins/access/email +$ make +$ ./build/teleport-email start +``` + +## Teleport User and Role Using Web UI or `tctl` CLI utility, create the role `access-email` and the user `access-email` belonging to the role `access-email`. You may use the following YAML declarations. -#### Role +### Role ```yaml kind: role @@ -30,10 +59,10 @@ spec: rules: - resources: ['access_request'] verbs: ['list', 'read', 'update'] -version: v4 +version: v5 ``` -#### User +### User ```yaml kind: user @@ -44,7 +73,7 @@ spec: version: v2 ``` -### Generate the certificate +## Generate the certificate For the plugin to connect to Auth Server, it needs an identity file containing TLS/SSH certificates. This can be obtained with tctl: @@ -54,7 +83,7 @@ $ tctl auth sign --auth-server=AUTH-SERVER:PORT --format=file --user=access-emai Here, `AUTH-SERVER:PORT` could be `localhost:3025`, `your-in-cluster-auth.example.com:3025`, `your-remote-proxy.example.com:3080` or `your-teleport-cloud.teleport.sh:443`. For non-localhost connections, you might want to pass the `--identity=...` option to authenticate yourself to Auth Server. -### Save configuration file +## Save configuration file By default, configuration file is expected to be at `/etc/teleport-email.toml`. @@ -88,21 +117,17 @@ output = "stderr" # Logger output. Could be "stdout", "stderr" or "/var/lib/tele severity = "INFO" # Logger severity. Could be "INFO", "ERROR", "DEBUG" or "WARN". ``` -### Run the plugin +## Run the plugin ```bash -teleport-email start +$ teleport-email start ``` -If something bad happens, try to run it with `-d` option i.e. `teleport-email start -d` and attach the stdout output to the issue you are going to create. - -## Building from source - -To build the plugin from source you need Go >= 1.16 and `make`. +or with docker: ```bash -git clone https://github.com/gravitational/teleport-plugins.git -cd teleport-plugins/access/email -make -./build/teleport-email start +$ docker run -v :/etc/teleport-email.toml quay.io/gravitational/access-plugin-email:9.0.2 start ``` + +If something bad happens, try to run it with `-d` option i.e. `teleport-email start -d` and attach the stdout output to the issue you are going to create. + diff --git a/access/gitlab/README.md b/access/gitlab/README.md index 51b54f71c..7752acf57 100644 --- a/access/gitlab/README.md +++ b/access/gitlab/README.md @@ -44,7 +44,7 @@ spec: rules: - resources: ['access_request'] verbs: ['list', 'read', 'update'] -version: v4 +version: v5 ``` #### User @@ -117,7 +117,7 @@ If for some reason you want to disable TLS termination in the plugin and deploy ## Building from source -To build the plugin from source you need Go >= 1.16 and `make`. +To build the plugin from source you need [Go](https://go.dev/) and `make`. ```bash git clone https://github.com/gravitational/teleport-plugins.git diff --git a/access/jira/README.md b/access/jira/README.md index ee6ff0105..8c80107df 100644 --- a/access/jira/README.md +++ b/access/jira/README.md @@ -4,9 +4,17 @@ This package provides Teleport <-> Jira integration that allows teams to approve or deny Access Requests on a Jira Project Board. It works with both Jira Cloud and Jira Server 8. -## Setup +## Install the plugin -### Install the plugin +There are several methods to installing and using the Teleport Jira Plugin: + +1. Use a [precompiled binary](#precompiled-binary) + +2. Use a [docker image](#docker-image) + +3. Install from [source](#building-from-source) + +### Precompiled Binary Get the plugin distribution. @@ -17,7 +25,30 @@ $ cd teleport-access-jira $ ./install ``` -### Set up Jira board +### Docker Image +```bash +$ docker pull quay.io/gravitational/access-plugin-jira:9.0.2 +``` + +```bash +$ docker run quay.io/gravitational/access-plugin-jira:9.0.2 version +teleport-jira v9.0.2 git:teleport-jira-v9.0.2-0-g9e149895 go1.17.8 +``` + +For a list of available tags, visit [https://quay.io/](https://quay.io/repository/gravitational/access-plugin-jira?tab=tags) + +### Building from source + +To build the plugin from source you need [Go](https://go.dev/) and `make`. + +```bash +$ git clone https://github.com/gravitational/teleport-plugins.git +$ cd teleport-plugins/access/jira +$ make +$ ./build/teleport-jira start +``` + +## Set up Jira board - [See detailed setup instructions for Jira Cloud on the website](https://goteleport.com/teleport/docs/enterprise/workflow/ssh_approval_jira_cloud/) - [See detailed setup instructions for Jira Server on the website](https://goteleport.com/teleport/docs/enterprise/workflow/ssh_approval_jira_server/) @@ -29,11 +60,11 @@ Setup process is different for the Jira Cloud and Jira Server editions: - Jira Server getting started guide: [INSTALL-JIRA-SERVER.md](./INSTALL-JIRA-SERVER.md) -### Teleport User and Role +## Teleport User and Role Using Web UI or `tctl` CLI utility, create the role `access-jira` and the user `access-jira` belonging to the role `access-jira`. You may use the following YAML declarations. -#### Role +### Role ```yaml kind: role @@ -44,10 +75,10 @@ spec: rules: - resources: ['access_request'] verbs: ['list', 'read', 'update'] -version: v4 +version: v5 ``` -#### User +### User ```yaml kind: user @@ -58,7 +89,7 @@ spec: version: v2 ``` -### Generate the certificate +## Generate the certificate For the plugin to connect to Auth Server, it needs an identity file containing TLS/SSH certificates. This can be obtained with tctl: @@ -68,7 +99,7 @@ $ tctl auth sign --auth-server=AUTH-SERVER:PORT --format=file --user=access-jira Here, `AUTH-SERVER:PORT` could be `localhost:3025`, `your-in-cluster-auth.example.com:3025`, `your-remote-proxy.example.com:3080` or `your-teleport-cloud.teleport.sh:443`. For non-localhost connections, you might want to pass the `--identity=...` option to authenticate yourself to Auth Server. -### Save configuration file +## Save configuration file By default, configuration file is expected to be at `/etc/teleport-jira.toml`. @@ -113,24 +144,20 @@ output = "stderr" # Logger output. Could be "stdout", "stderr" or "/var/lib/tele severity = "INFO" # Logger severity. Could be "INFO", "ERROR", "DEBUG" or "WARN". ``` -### Run the plugin +## Run the plugin ```bash -teleport-jira start +$ teleport-jira start ``` -If something bad happens, try to run it with `-d` option i.e. `teleport-jira start -d` and attach the stdout output to the issue you are going to create. +or with docker: -If for some reason you want to disable TLS termination in the plugin and deploy it somewhere else e.g. on some reverse proxy, you may want to run the plugin with `--insecure-no-tls` option. With `--insecure-no-tls` option, plugin's webhook server will talk plain HTTP protocol. +```bash +$ docker run -v :/etc/teleport-jira.toml quay.io/gravitational/access-plugin-jira:9.0.2 start +``` -## Building from source +If something bad happens, try to run it with `-d` option i.e. `teleport-jira start -d` and attach the stdout output to the issue you are going to create. -To build the plugin from source you need Go >= 1.16 and `make`. +If for some reason you want to disable TLS termination in the plugin and deploy it somewhere else e.g. on some reverse proxy, you may want to run the plugin with `--insecure-no-tls` option. With `--insecure-no-tls` option, plugin's webhook server will talk plain HTTP protocol. -```bash -git clone https://github.com/gravitational/teleport-plugins.git -cd teleport-plugins/access/jira -make -./build/teleport-jira start -``` diff --git a/access/mattermost/README.md b/access/mattermost/README.md index 9e3c217a2..3f22ed22e 100644 --- a/access/mattermost/README.md +++ b/access/mattermost/README.md @@ -3,11 +3,9 @@ This package provides Teleport <-> Mattermost integrataion that allows teams to get notified about new access requests in Mattermost. -## Setup - [See setup instructions on Teleport's website](https://goteleport.com/teleport/docs/enterprise/workflow/ssh_approval_mattermost/) -### Prerequisites +## Prerequisites This guide assumes that you have: @@ -15,7 +13,51 @@ This guide assumes that you have: - Admin privileges with access to `tctl` - Mattermost account with admin privileges. -#### Setting up a sandbox Mattermost instance for testing +## Install the plugin + +There are several methods to installing and using the Teleport Mattermost Plugin: + +1. Use a [precompiled binary](#precompiled-binary) + +2. Use a [docker image](#docker-image) + +3. Install from [source](#building-from-source) + +### Precompiled Binary + +Get the plugin distribution. + +```bash +$ curl -L https://get.gravitational.com/teleport-access-mattermost-v7.0.2-linux-amd64-bin.tar.gz +$ tar -xzf teleport-access-mattermost-v7.0.2-linux-amd64-bin.tar.gz +$ cd teleport-access-mattermost +$ ./install +``` + +### Docker Image +```bash +$ docker pull quay.io/gravitational/access-plugin-mattermost:9.0.2 +``` + +```bash +$ docker run quay.io/gravitational/access-plugin-mattermost:9.0.2 version +teleport-mattermost v9.0.2 git:teleport-mattermost-v9.0.2-0-g9e149895 go1.17.8 +``` + +For a list of available tags, visit [https://quay.io/](https://quay.io/repository/gravitational/access-plugin-mattermost?tab=tags) + +### Building from source + +To build the plugin from source you need [Go](https://go.dev/) and `make`. + +```bash +$ git clone https://github.com/gravitational/teleport-plugins.git +$ cd teleport-plugins/access/mattermost +$ make +$ ./build/teleport-mattermost start +``` + +## Setting up a sandbox Mattermost instance for testing If you want to build the plugin and test it with Mattermost, the easiest way to get Mattermost running is with the docker image: @@ -27,7 +69,7 @@ docker run --name mattermost-preview -d --publish 8065:8065 --add-host dockerhos Check out [more documentation on running Mattermost](https://docs.mattermost.com/install/docker-local-machine.html). -#### Setting up Mattermost to work with the plugin +### Setting up Mattermost to work with the plugin In Mattermost, go to System Console -> Integrations -> Enable Bot Account Creation -> Set to True. This will allow us to create a new bot account that the @@ -40,100 +82,52 @@ The new bot account will need Post All permission. The confirmation screen after you've created the bot will give you the access token. We'll use this in the config later. -#### Create an access-plugin role and user within Teleport +## Teleport User and Role -First off, using an existing Teleport Cluster, we are going to create a new -Teleport User and Role to access Teleport. +Using Web UI or `tctl` CLI utility, create the role `access-mattermost` and the user `access-mattermost` belonging to the role `access-mattermost`. You may use the following YAML declarations. -#### Create User and Role for access. +### Role -Log into Teleport Authentication Server, this is where you normally run `tctl`. -Don't change the username and the role name, it should be `access-plugin` for -the plugin to work correctly. - -```bash -$ cat > rscs.yaml < /etc/teleport-mattermost.yml +$ teleport-mattermost configure > /etc/teleport-mattermost.yml ``` Then, edit the config as needed. @@ -141,10 +135,21 @@ Then, edit the config as needed. ```TOML # example mattermost configuration TOML file [teleport] -addr = "example.com:3025" # Teleport Auth Server GRPC API address -client_key = "/var/lib/teleport/plugins/mattermost/auth.key" # Teleport GRPC client secret key -client_crt = "/var/lib/teleport/plugins/mattermost/auth.crt" # Teleport GRPC client certificate -root_cas = "/var/lib/teleport/plugins/mattermost/auth.cas" # Teleport cluster CA certs +# Teleport Auth/Proxy Server address. +# +# Should be port 3025 for Auth Server and 3080 or 443 for Proxy. +# For Teleport Cloud, should be in the form "your-account.teleport.sh:443". +addr = "example.com:3025" + +# Credentials. +# +# When using --format=file: +# identity = "/var/lib/teleport/plugins/mattermost/auth_id" # Identity file +# +# When using --format=tls: +# client_key = "/var/lib/teleport/plugins/mattermost/auth.key" # Teleport TLS secret key +# client_crt = "/var/lib/teleport/plugins/mattermost/auth.crt" # Teleport TLS certificate +# root_cas = "/var/lib/teleport/plugins/mattermost/auth.cas" # Teleport CA certs [mattermost] url = "https://mattermost.example.com" # Mattermost Server URL @@ -155,22 +160,23 @@ output = "stderr" # Logger output. Could be "stdout", "stderr" or "/var/lib/tele severity = "INFO" # Logger severity. Could be "INFO", "ERROR", "DEBUG" or "WARN". ``` -To use with Teleport Cloud, you should set a path to identity file exported with `--format=file` option. +## Running the plugin -```TOML -[teleport] -addr = "yourproxy.teleport.sh:443" # Teleport proxy address -identity = "/var/lib/teleport/plugins/mattermost/auth" # Teleport identity file +With the config above, you should be able to run the bot invoking + +```bash +$ teleport-mattermost start ``` -### Running the plugin +or with docker: -With the config above, you should be able to run the bot invoking -`teleport-mattermost start` +```bash +$ docker run -v :/etc/teleport-mattermost.toml quay.io/gravitational/access-plugin-mattermost:9.0.2 start +``` -### The Workflow +## The Workflow -#### Create an access request +### Create an access request You can create an access request using Web UI going to `https://your-proxy.example.com/web/requests/new` where your-proxy.example.com @@ -180,11 +186,11 @@ Check that you see a request message on Mattermost. It should look like this: %image% -#### Review the request +### Review the request Open the Link in message and choose to either approve or deny the request. The messages should automatically get updated to reflect the action you just did. -### Teleport OSS edition +## Teleport OSS edition Currently, Teleport OSS edition does not have an "Access Requests" page at Web UI. Alternatively, you can create an access request using tsh: diff --git a/access/pagerduty/README.md b/access/pagerduty/README.md index 3cd2f6038..ebe27469a 100644 --- a/access/pagerduty/README.md +++ b/access/pagerduty/README.md @@ -14,90 +14,98 @@ This guide assumes you have - Teleport Enterprise 6.1 or newer with admin permissions and access to `tctl` - Pagerduty account already set, with access to creating a new API token. -### Create an access-plugin role and user within Teleport +## Install the plugin -First off, using an existing Teleport Cluster, we are going to create a new -Teleport User and Role to access Teleport. +There are several methods to installing and using the Teleport Pagerduty Plugin: -#### Create User and Role for access. +1. Use a [precompiled binary](#precompiled-binary) -Log into Teleport Authent Server, this is where you normally run `tctl`. Don't -change the username and the role name, it should be `access-plugin` for the -plugin to work correctly. +2. Use a [docker image](#docker-image) -_Note: if you're using other plugins, you might want to create different users -and roles for different plugins_. +3. Install from [source](#building-from-source) +### Precompiled Binary + +Get the plugin distribution. + +```bash +$ curl -L https://get.gravitational.com/teleport-access-pagerduty-v7.0.2-linux-amd64-bin.tar.gz +$ tar -xzf teleport-access-pagerduty-v7.0.2-linux-amd64-bin.tar.gz +$ cd teleport-access-pagerduty +$ ./install ``` -$ cat > rscs.yaml < API Access -> Create New API Key**, add a key description, and save the key. We'll use the key in the plugin config file later. -### Setting up Pagerduty notification alerts +## Setting up Pagerduty notification alerts -Once a new access request has been created, plugin creates an incident in a Pagerduty service. In order to know what service to post the notification in, the service name must be set up in a request annotation of a role of a user who requests an access. +Once a new access request has been created, the plugin creates an incident in a Pagerduty service. In order to know what service to post the notification in, the service name must be set up in a request annotation of a role of a user who requests an access. Suppose you created a Pagerduty service called "Teleport Notifications" and want it to be notified about all new access requests from users under role `challenger`. Then you should set up a request annotation called `pagerduty_notify_service` containing a list with an only element `["Teleport notifications"]`. @@ -113,7 +121,7 @@ spec: pagerduty_notify_service: ["teleport notifications"] ``` -### Setting up auto-approval behavior +## Setting up auto-approval behavior If given sufficient permissions, Pagerduty plugin can auto-approve new access requests if they come from a user who is currently on-call. More specifically, it works like this: @@ -148,42 +156,18 @@ spec: - `alice@example.com` requests a role `champion`. - Then pagerduty plugin **submits an approval** of Alice's request. -## Install - -### Installing - -```bash - -# Check out the repo -git clone https://github.com/gravitational/teleport-plugins.git -cd teleport-plugins -# Build the bot -make access-pagerduty - -# Configure the plugin -./access/pagerduty/build/teleport-pagerduty configure > teleport-pagertudy.toml - -# Run the plugin, assuming you have teleport running: -./build/teleport-pagerduty start -``` - -The teleport-pagerduty executable should be placed onto a server that can access -the auth server address. - -### Config file +## Configuring Pagerduty Plugin Teleport Pagerduty plugin has its own configuration file in TOML format. Before starting the plugin for the first time, you'll need to generate and edit that config file. ```bash -teleport-pagerduty configure > /etc/teleport-pagerduty.toml +$ teleport-pagerduty configure > /etc/teleport-pagerduty.toml ``` -#### Editing the config file - -Afger generating the config, edit it as follows: +Then, edit the config as needed. ```TOML # example teleport-pagerduty configuration TOML file @@ -202,11 +186,17 @@ output = "stderr" # Logger output. Could be "stdout", "stderr" or "/var/lib/tele severity = "INFO" # Logger severity. Could be "INFO", "ERROR", "DEBUG" or "WARN". ``` -### Running the plugin +## Running the plugin + +By default, `teleport-pagerduty` will assume it's config is in +`/etc/teleport-pagerduty.toml`, but you can override it with `--config` option. ``` -teleport-pagerduty start +$ teleport-pagerduty start ``` -By default, `teleport-pagerduty` will assume it's config is in -`/etc/teleport-pagerduty.toml`, but you can override it with `--config` option. +or with docker: + +```bash +$ docker run -v :/etc/teleport-pagerduty.toml quay.io/gravitational/access-plugin-pagerduty:9.0.2 start +``` diff --git a/access/slack/README.md b/access/slack/README.md index fd4623f5c..4f55b349e 100644 --- a/access/slack/README.md +++ b/access/slack/README.md @@ -1,19 +1,161 @@ # Teleport Slack Plugin -This package implements a simple Slack plugin using the API provided in the -[`access`](../) package which notifies about Access Requests via Slack -messages. +This package implements a simple Slack plugin using the Teleport Access API. A slack channel receives an alert when an access request is created. ## Setup [See setup instructions on Teleport's website](https://goteleport.com/teleport/docs/enterprise/workflow/ssh_approval_slack/) -You must have Go version 1.15 or higher to build. +Detailed install steps are provided in our [docs](https://goteleport.com/docs/enterprise/workflow/ssh-approval-slack/). -Run `make access-slack` from the repository root to build the slack plugin. Then -you can find it in `./access/slack/build/teleport-slack`. +## Install the plugin -Detailed install steps are provided in our [docs](https://goteleport.com/docs/enterprise/workflow/ssh-approval-slack/). +There are several methods to installing and using the Teleport Slack Plugin: + +1. Use a [precompiled binary](#precompiled-binary) + +2. Use a [docker image](#docker-image) + +3. Install from [source](#building-from-source) + +### Precompiled Binary + +Get the plugin distribution. + +```bash +$ curl -L https://get.gravitational.com/teleport-access-slack-v7.0.2-linux-amd64-bin.tar.gz +$ tar -xzf teleport-access-slack-v7.0.2-linux-amd64-bin.tar.gz +$ cd teleport-access-slack +$ ./install +``` + +### Docker Image +```bash +$ docker pull quay.io/gravitational/access-plugin-slack:9.0.2 +``` + +```bash +$ docker run quay.io/gravitational/access-plugin-slack:9.0.2 version +teleport-slack v9.0.2 git:teleport-slack-v9.0.2-0-g9e149895 go1.17.8 +``` + +For a list of available tags, visit [https://quay.io/](https://quay.io/repository/gravitational/access-plugin-slack?tab=tags) + +### Building from source + +To build the plugin from source you need [Go](https://go.dev/) and `make`. + +```bash +$ git clone https://github.com/gravitational/teleport-plugins.git +$ cd teleport-plugins/access/slack +$ make +$ ./build/teleport-slack start +``` + + + +## Teleport User and Role + +Using Web UI or `tctl` CLI utility, create the role `access-slack` and the user `access-slack` belonging to the role `access-slack`. You may use the following YAML declarations. + +### Role + +```yaml +kind: role +metadata: + name: access-slack +spec: + allow: + rules: + - resources: ['access_request'] + verbs: ['list', 'read', 'update'] +version: v5 +``` + +### User + +```yaml +kind: user +metadata: + name: access-slack +spec: + roles: ['access-slack'] +version: v2 +``` + +## Generate the certificate + +For the plugin to connect to Auth Server, it needs an identity file containing TLS/SSH certificates. This can be obtained with tctl: + +```bash +$ tctl auth sign --auth-server=AUTH-SERVER:PORT --format=file --user=access-slack --out=/var/lib/teleport/plugins/slack/auth_id --ttl=8760h +``` + +Here, `AUTH-SERVER:PORT` could be `localhost:3025`, `your-in-cluster-auth.example.com:3025`, `your-remote-proxy.example.com:3080` or `your-teleport-cloud.teleport.sh:443`. For non-localhost connections, you might want to pass the `--identity=...` option to authenticate yourself to Auth Server. + +## Configuring Slack Plugin + +Slack Plugin uses a config file in TOML format. Generate a boilerplate config +by running the following command: + +``` +$ teleport-slack configure > /etc/teleport-slack.yml +``` + +Then, edit the config as needed. + +```TOML +# Example slack plugin configuration TOML file + +[teleport] +# Teleport Auth/Proxy Server address. +# addr = "example.com:3025" +# +# Should be port 3025 for Auth Server and 3080 or 443 for Proxy. +# For Teleport Cloud, should be in the form "your-account.teleport.sh:443". + +# Credentials generated with `tctl auth sign`. +# +# When using --format=file: +# identity = "/var/lib/teleport/plugins/slack/auth_id" # Identity file +# +# When using --format=tls: +# client_key = "/var/lib/teleport/plugins/slack/auth.key" # Teleport TLS secret key +# client_crt = "/var/lib/teleport/plugins/slack/auth.crt" # Teleport TLS certificate +# root_cas = "/var/lib/teleport/plugins/slack/auth.cas" # Teleport CA certs + +[slack] +# Slack Bot OAuth token +token = "xoxb-11xx" + +[role_to_recipients] +# Map roles to recipients. +# +# Provide slack user_email/channel recipients for access requests for specific roles. +# role.suggested_reviewers will automatically be treated as additional email recipients. +# "*" must be provided to match non-specified roles. +# +# "dev" = "devs-slack-channel" +# "*" = ["admin@email.com", "admin-slack-channel"] + +[log] +output = "stderr" # Logger output. Could be "stdout", "stderr" or "/var/lib/teleport/slack.log" +severity = "INFO" # Logger severity. Could be "INFO", "ERROR", "DEBUG" or "WARN". +``` + +## Running the plugin + +With the config above, you should be able to run the bot invoking + +```bash +$ teleport-slack start +``` + +or with docker: + +```bash +$ docker run -v :/etc/teleport-slack.toml quay.io/gravitational/access-plugin-slack:9.0.2 start +``` ## Usage diff --git a/access/slack/example_config.toml b/access/slack/example_config.toml index e7439bebd..0f77af76c 100644 --- a/access/slack/example_config.toml +++ b/access/slack/example_config.toml @@ -1,8 +1,6 @@ # Example slack plugin configuration TOML file [teleport] -# This section - # Teleport Auth/Proxy Server address. # addr = "example.com:3025" # diff --git a/event-handler/README.md b/event-handler/README.md index d0d01a8d0..12c3454b8 100644 --- a/event-handler/README.md +++ b/event-handler/README.md @@ -13,18 +13,47 @@ This guide assumes that you have: The required Fluentd version for production setup is v1.12.4 or newer. Lower versions do not support TLS. -## Installing the plugin +## Install the plugin -We recommend installing the Teleport Plugins alongside the Teleport Proxy. This is an ideal location as plugins have a low memory footprint, and will require both public internet access and Teleport Auth access. We currently only provide linux-amd64 binaries, you can also compile these plugins from source. +There are several methods to installing and using the Teleport Event Handler Plugin: -### Install the plugin from source +1. Use a [precompiled binary](#precompiled-binary) + +2. Use a [docker image](#docker-image) + +3. Install from [source](#building-from-source) + +### Precompiled Binary + +Get the plugin distribution. + +```bash +$ curl -L https://get.gravitational.com/teleport-event-handler-v7.0.2-linux-amd64-bin.tar.gz +$ tar -xzf teleport-event-handler-v7.0.2-linux-amd64-bin.tar.gz +$ cd teleport-event-handler +$ ./install +``` + +### Docker Image +```bash +$ docker pull quay.io/gravitational/event-handler-plugin:9.0.2 +``` + +```bash +$ docker run quay.io/gravitational/event-handler-plugin:9.0.2 version +Teleport event handler v9.0.2 git:teleport-event-handler-v9.0.2-0-g9e149895 go1.17.8 +``` + +For a list of available tags, visit [https://quay.io/](https://quay.io/repository/gravitational/event-handler-plugin?tab=tags) + +### Building from source Please ensure that Docker is running! ```sh -git clone https://github.com/gravitational/teleport-plugins.git --depth 1 -cd teleport-plugins/event-handler/build.assets -make install +$ git clone https://github.com/gravitational/teleport-plugins.git --depth 1 +$ cd teleport-plugins/event-handler/build.assets +$ make install ``` This command will build `build/teleport-event-handler` executable and place it to `/usr/local/bin` folder. The following error means that you do not have write permissions on target folder: @@ -36,45 +65,43 @@ cp: /usr/local/bin/teleport-event-handler: Operation not permitted To fix this, you can either set target folder to something listed in your `$PATH`: ```sh -make install BINDIR=/tmp/test-fluentd-setup +$ make install BINDIR=/tmp/test-fluentd-setup ``` or copy binary file manually with `sudo`: ```sh -sudo cp build/teleport-event-handler /usr/local/bin +$ sudo cp build/teleport-event-handler /usr/local/bin ``` -In sections below we assume that you have `teleport-event-handler` executable available in your `$PATH` or cwd. - ## Generate example configuration Run: ```sh -teleport-event-handler configure . +$ teleport-event-handler configure . ``` You'll see the following output: ```sh -Teleport event handler 0.0.1 07617b0ad0829db043fe779faf1669defdc8d84e +Teleport event handler 9.0.2 teleport-event-handler-v9.0.2-0-g9e149895 -[1] mTLS Fluentd certificates generated and saved to ca.crt, ca.key, server.crt, server.key, client.crt, client.key +[1] Generated mTLS Fluentd certificates ca.crt, ca.key, server.crt, server.key, client.crt, client.key [2] Generated sample teleport-event-handler role and user file teleport-event-handler-role.yaml [3] Generated sample fluentd configuration file fluent.conf [4] Generated plugin configuration file teleport-event-handler.toml Follow-along with our getting started guide: -https://goteleport.com/setup/guides/fluentd +https://goteleport.com/docs/setup/guides/fluentd ``` Where `ca.crt` and `ca.key` would be Fluentd self-signed CA certificate and private key, `server.crt` and `server.key` would be fluentd server certificate and key, `client.crt` and `client.key` would be Fluentd client certificate and key, all signed by the generated CA. Check ```teleport-event-handler configure --help``` usage instructions. You may set several configuration options, including key/cert file names, server key encryption password and Teleport auth proxy address. -### Create user and role for access audit log events +## Create user and role for access audit log events The generated `teleport-event-handler-role.yaml` would contain the following content: @@ -94,7 +121,7 @@ spec: rules: - resources: ['event','session'] verbs: ['list','read'] -version: v4 +version: v5 ``` It defines `teleport-event-handler` role and user which has read-only access to the `event` API. @@ -105,7 +132,7 @@ Log into Teleport Authentication Server, this is where you normally run `tctl`. tctl create -f teleport-event-handler-role.yaml ``` -### Export teleport-event-handler identity file +## Export teleport-event-handler identity file Teleport Plugin use the fluentd role and user to read the events. We export the identity files, using tctl auth sign. @@ -115,7 +142,7 @@ tctl auth sign --out identity --user teleport-event-handler This will generate `identity` which contains TLS certificates and will be used to connect plugin to your Teleport instance. -### Run fluentd +## Run fluentd The plugin will send events to the fluentd instance using keys generated on the previous step. Generated `fluent.conf` file would contain the following content: @@ -161,7 +188,7 @@ Start fluentd instance: docker run -p 8888:8888 -v $(pwd):/keys -v $(pwd)/fluent.conf:/fluentd/etc/fluent.conf fluent/fluentd:edge ``` -### Configure the plugin +## Configure the plugin The generated `teleport-event-handler.toml` would contain the following plugin configuration: @@ -183,10 +210,16 @@ addr = "localhost:3025" # Default local Teleport instance address identity = "identity" # Identity file exported on previous step ``` -### Start the plugin +## Start the plugin ```sh -teleport-event-handler start --config teleport-event-handler.toml --start-time 2021-01-01T00:00:00Z +$ teleport-event-handler start --config teleport-event-handler.toml --start-time 2021-01-01T00:00:00Z +``` + +or with docker: + +```sh +$ docker run -v :/etc/teleport-event-handler quay.io/gravitational/event-handler-plugin:9.0.2 start --config /etc/teleport-event-handler/teleport-event-handler.toml --start-time 2021-01-01T00:00:00Z ``` Note that here we used start time at the beginning of year 2021. Supposedly you have some events at the Teleport instance you are connecting to. Otherwise, you can omit `--start-time` flag, start the service and generate an events using `tctl create -f teleport-event-handler.yaml` then from the first step. `teleport-event-handler` will wait for that new events to appear and will send them to the fluentd. @@ -263,13 +296,13 @@ You could use `--dry-run` argument if you want event handler to simulate event e ### Login to Teleport cloud: ```sh - tsh login --proxy test.teleport.sh:443 --user test@evilmartians.com - ``` +$ tsh login --proxy test.teleport.sh:443 --user test@evilmartians.com +``` ### Generate sample configuration using the cloud address: ```sh -teleport-event-handler configure . test.teleport.sh:443 +$ teleport-event-handler configure . test.teleport.sh:443 ``` Then follow the manual starting at ["Export teleport-event-handler identity file"](#export) section. From 053ad1bcfbbf3b59362bf792e171de1f25d7976d Mon Sep 17 00:00:00 2001 From: Roman Tkachenko Date: Thu, 31 Mar 2022 20:18:51 -0700 Subject: [PATCH 02/18] Release 9.0.3 (#478) --- access/email/Makefile | 2 +- access/jira/Makefile | 2 +- access/mattermost/Makefile | 2 +- access/pagerduty/Makefile | 2 +- access/slack/Makefile | 2 +- event-handler/Makefile | 2 +- terraform/install.mk | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/access/email/Makefile b/access/email/Makefile index 38a45b16b..02e97c2b4 100644 --- a/access/email/Makefile +++ b/access/email/Makefile @@ -1,4 +1,4 @@ -VERSION=9.0.2 +VERSION=9.0.3 GO_VERSION=1.17.8 BUILDDIR ?= build diff --git a/access/jira/Makefile b/access/jira/Makefile index 0e714e9c0..52e3eb72e 100644 --- a/access/jira/Makefile +++ b/access/jira/Makefile @@ -1,4 +1,4 @@ -VERSION=9.0.2 +VERSION=9.0.3 GO_VERSION=1.17.8 BUILDDIR ?= build diff --git a/access/mattermost/Makefile b/access/mattermost/Makefile index 287fc64ba..eebbaff9b 100644 --- a/access/mattermost/Makefile +++ b/access/mattermost/Makefile @@ -1,4 +1,4 @@ -VERSION=9.0.2 +VERSION=9.0.3 GO_VERSION=1.17.8 BUILDDIR ?= build diff --git a/access/pagerduty/Makefile b/access/pagerduty/Makefile index b89afcccc..d4ff6ec60 100644 --- a/access/pagerduty/Makefile +++ b/access/pagerduty/Makefile @@ -1,4 +1,4 @@ -VERSION=9.0.2 +VERSION=9.0.3 GO_VERSION=1.17.8 BUILDDIR ?= build diff --git a/access/slack/Makefile b/access/slack/Makefile index 985086d5f..139917327 100644 --- a/access/slack/Makefile +++ b/access/slack/Makefile @@ -1,4 +1,4 @@ -VERSION=9.0.2 +VERSION=9.0.3 GO_VERSION=1.17.8 BUILDDIR ?= build diff --git a/event-handler/Makefile b/event-handler/Makefile index 5a1277189..c71d0dba2 100644 --- a/event-handler/Makefile +++ b/event-handler/Makefile @@ -1,4 +1,4 @@ -VERSION=9.0.2 +VERSION=9.0.3 GO_VERSION=1.17.8 OS ?= $(shell go env GOOS) diff --git a/terraform/install.mk b/terraform/install.mk index 6e9808cf8..8d92feeb0 100644 --- a/terraform/install.mk +++ b/terraform/install.mk @@ -1,4 +1,4 @@ -VERSION=9.0.2 +VERSION=9.0.3 OS ?= $(shell go env GOOS) ARCH ?= $(shell go env GOARCH) From e6fc3cf35a0da19d75b9a7cd4890f6033ad89423 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Andr=C3=A9=20Dinis?= Date: Fri, 1 Apr 2022 09:06:09 +0100 Subject: [PATCH 03/18] TF test: session rec must check host (#475) We had a test validating the change of a variable in SessionRecordingConfig However, this variable must always be true when we are running the enterprise (or cloud) version https://github.com/gravitational/teleport/blob/809e60c318cf98207f0db40795faff5020baaa07/lib/modules/modules.go#L132 We ended up changing the test's target to another variable which should be just as good as the current one: recording mode. This way we can use either the enterprise or the OSS version and the test should pass --- terraform/test/fixtures/session_recording_config_0_set.tf | 1 + .../test/fixtures/session_recording_config_1_update.tf | 3 ++- terraform/test/session_recording_config_test.go | 8 ++++---- terraform/tfschema/types_terraform.go | 6 +++--- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/terraform/test/fixtures/session_recording_config_0_set.tf b/terraform/test/fixtures/session_recording_config_0_set.tf index 0124ac63b..0799e6113 100644 --- a/terraform/test/fixtures/session_recording_config_0_set.tf +++ b/terraform/test/fixtures/session_recording_config_0_set.tf @@ -7,6 +7,7 @@ resource "teleport_session_recording_config" "test" { } spec = { + mode = "node" proxy_checks_host_keys = true } } \ No newline at end of file diff --git a/terraform/test/fixtures/session_recording_config_1_update.tf b/terraform/test/fixtures/session_recording_config_1_update.tf index 49cd91c1e..9a9aab0b7 100644 --- a/terraform/test/fixtures/session_recording_config_1_update.tf +++ b/terraform/test/fixtures/session_recording_config_1_update.tf @@ -7,6 +7,7 @@ resource "teleport_session_recording_config" "test" { } spec = { - proxy_checks_host_keys = false + mode = "off" + proxy_checks_host_keys = true } } \ No newline at end of file diff --git a/terraform/test/session_recording_config_test.go b/terraform/test/session_recording_config_test.go index c3c4afd30..267e06dfa 100644 --- a/terraform/test/session_recording_config_test.go +++ b/terraform/test/session_recording_config_test.go @@ -34,7 +34,7 @@ func (s *TerraformSuite) TestSessionRecordingConfig() { Config: s.getFixture("session_recording_config_0_set.tf"), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(name, "kind", "session_recording_config"), - resource.TestCheckResourceAttr(name, "spec.proxy_checks_host_keys", "true"), + resource.TestCheckResourceAttr(name, "spec.mode", "node"), ), }, { @@ -45,7 +45,7 @@ func (s *TerraformSuite) TestSessionRecordingConfig() { Config: s.getFixture("session_recording_config_1_update.tf"), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(name, "kind", "session_recording_config"), - resource.TestCheckResourceAttr(name, "spec.proxy_checks_host_keys", "false"), + resource.TestCheckResourceAttr(name, "spec.mode", "off"), ), }, { @@ -64,7 +64,7 @@ func (s *TerraformSuite) TestImportSessionRecordingConfig() { sessionrRecordingConfig := &types.SessionRecordingConfigV2{ Metadata: types.Metadata{}, Spec: types.SessionRecordingConfigSpecV2{ - ProxyChecksHostKeys: types.NewBoolOption(true), + Mode: "off", }, } err := sessionrRecordingConfig.CheckAndSetDefaults() @@ -83,7 +83,7 @@ func (s *TerraformSuite) TestImportSessionRecordingConfig() { ImportStateId: id, ImportStateCheck: func(state []*terraform.InstanceState) error { require.Equal(s.T(), state[0].Attributes["kind"], "session_recording_config") - require.Equal(s.T(), state[0].Attributes["spec.proxy_checks_host_keys"], "true") + require.Equal(s.T(), state[0].Attributes["spec.mode"], "off") return nil }, diff --git a/terraform/tfschema/types_terraform.go b/terraform/tfschema/types_terraform.go index b49e2465f..a888595a9 100644 --- a/terraform/tfschema/types_terraform.go +++ b/terraform/tfschema/types_terraform.go @@ -26,7 +26,6 @@ import ( _ "github.com/gogo/protobuf/gogoproto" proto "github.com/gogo/protobuf/proto" - _ "github.com/golang/protobuf/ptypes/timestamp" github_com_gravitational_teleport_api_constants "github.com/gravitational/teleport/api/constants" github_com_gravitational_teleport_api_types "github.com/gravitational/teleport/api/types" github_com_hashicorp_terraform_plugin_framework_attr "github.com/hashicorp/terraform-plugin-framework/attr" @@ -34,6 +33,7 @@ import ( github_com_hashicorp_terraform_plugin_framework_tfsdk "github.com/hashicorp/terraform-plugin-framework/tfsdk" github_com_hashicorp_terraform_plugin_framework_types "github.com/hashicorp/terraform-plugin-framework/types" github_com_hashicorp_terraform_plugin_go_tftypes "github.com/hashicorp/terraform-plugin-go/tftypes" + _ "google.golang.org/protobuf/types/known/timestamppb" ) // Reference imports to suppress errors if they are not otherwise used. @@ -1578,7 +1578,7 @@ func GenSchemaRoleV5(ctx context.Context) (github_com_hashicorp_terraform_plugin Optional: true, PlanModifiers: []github_com_hashicorp_terraform_plugin_framework_tfsdk.AttributePlanModifier{github_com_hashicorp_terraform_plugin_framework_tfsdk.UseStateForUnknown()}, Type: github_com_hashicorp_terraform_plugin_framework_types.StringType, - Validators: []github_com_hashicorp_terraform_plugin_framework_tfsdk.AttributeValidator{UseVersionBetween(3, 4)}, + Validators: []github_com_hashicorp_terraform_plugin_framework_tfsdk.AttributeValidator{UseVersionBetween(3, 5)}, }, }}, nil } @@ -1863,7 +1863,7 @@ func GenSchemaOIDCConnectorV3(ctx context.Context) (github_com_hashicorp_terrafo Optional: true, PlanModifiers: []github_com_hashicorp_terraform_plugin_framework_tfsdk.AttributePlanModifier{github_com_hashicorp_terraform_plugin_framework_tfsdk.UseStateForUnknown()}, Type: github_com_hashicorp_terraform_plugin_framework_types.StringType, - Validators: []github_com_hashicorp_terraform_plugin_framework_tfsdk.AttributeValidator{UseVersionBetween(2, 2)}, + Validators: []github_com_hashicorp_terraform_plugin_framework_tfsdk.AttributeValidator{UseVersionBetween(3, 3)}, }, }}, nil } From b20ab7d37da988bb0922d8617a9d77828108192f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Andr=C3=A9=20Dinis?= Date: Mon, 4 Apr 2022 17:33:41 +0100 Subject: [PATCH 04/18] TF SAML Connector: require any of (EntityDescriptor, EntityDescriptorURL) (#467) For Terraform SAML Connector, at least one of the following Spec attributes must exist: - EntityDescriptor - EntityDescriptorURL However, we were always requiring the EntityDescriptor We removed this field as required Things would work out because the server would validate that either one of them is present. The server returns an error if both are empty. However, to improve the UX we added a `AnyOf` validation to the terraform side It will validate before hitting the server Fixes #448 --- terraform/protoc-gen-terraform-teleport.yaml | 3 +- ...ector_0_create_with_entitydescriptorurl.tf | 20 +++++ ...ector_0_create_without_entitydescriptor.tf | 19 ++++ terraform/test/saml_connector_test.go | 25 ++++++ terraform/tfschema/types_terraform.go | 3 +- terraform/tfschema/validators.go | 65 ++++++++++++++ terraform/tfschema/validators_test.go | 90 +++++++++++++++++++ 7 files changed, 223 insertions(+), 2 deletions(-) create mode 100644 terraform/test/fixtures/saml_connector_0_create_with_entitydescriptorurl.tf create mode 100644 terraform/test/fixtures/saml_connector_0_create_without_entitydescriptor.tf create mode 100644 terraform/tfschema/validators_test.go diff --git a/terraform/protoc-gen-terraform-teleport.yaml b/terraform/protoc-gen-terraform-teleport.yaml index c9fc19693..37645c11f 100644 --- a/terraform/protoc-gen-terraform-teleport.yaml +++ b/terraform/protoc-gen-terraform-teleport.yaml @@ -251,7 +251,6 @@ required_fields: # SAML connector - "SAMLConnectorV2.Spec" - "SAMLConnectorV2.Spec.AssertionConsumerService" - - "SAMLConnectorV2.Spec.EntityDescriptor" - "SAMLConnectorV2.Spec.AttributesToRoles" - "SAMLConnectorV2.Metadata.Name" @@ -317,6 +316,8 @@ validators: - UseVersionBetween(3,5) SAMLConnectorV2.Version: - UseVersionBetween(2,2) + SAMLConnectorV2.Spec: + - UseAnyOfValidator("entity_descriptor", "entity_descriptor_url") SessionRecordingConfigV2.Version: - UseVersionBetween(2,2) SessionRecordingConfigV2.Metadata.Labels: diff --git a/terraform/test/fixtures/saml_connector_0_create_with_entitydescriptorurl.tf b/terraform/test/fixtures/saml_connector_0_create_with_entitydescriptorurl.tf new file mode 100644 index 000000000..b98758001 --- /dev/null +++ b/terraform/test/fixtures/saml_connector_0_create_with_entitydescriptorurl.tf @@ -0,0 +1,20 @@ +resource "teleport_saml_connector" "test" { + metadata = { + name = "test" + expires = "2022-10-12T07:20:50Z" + labels = { + example = "yes" + } + } + + spec = { + attributes_to_roles = [{ + name = "groups" + roles = ["admin"] + value = "okta-admin" + }] + + acs = "https://example.com/v1/webapi/saml/acs" + entity_descriptor_url = "https://dev-84961217.okta.com/app/exk4d7tmnz9DEaEw85d7/sso/saml/metadata" + } +} \ No newline at end of file diff --git a/terraform/test/fixtures/saml_connector_0_create_without_entitydescriptor.tf b/terraform/test/fixtures/saml_connector_0_create_without_entitydescriptor.tf new file mode 100644 index 000000000..3df1b223f --- /dev/null +++ b/terraform/test/fixtures/saml_connector_0_create_without_entitydescriptor.tf @@ -0,0 +1,19 @@ +resource "teleport_saml_connector" "test" { + metadata = { + name = "test" + expires = "2022-10-12T07:20:50Z" + labels = { + example = "yes" + } + } + + spec = { + attributes_to_roles = [{ + name = "groups" + roles = ["admin"] + value = "okta-admin" + }] + + acs = "https://example.com/v1/webapi/saml/acs" + } +} \ No newline at end of file diff --git a/terraform/test/saml_connector_test.go b/terraform/test/saml_connector_test.go index 03aaffac6..9f9c5c0f9 100644 --- a/terraform/test/saml_connector_test.go +++ b/terraform/test/saml_connector_test.go @@ -17,6 +17,8 @@ limitations under the License. package test import ( + "regexp" + "github.com/gravitational/teleport/api/types" "github.com/gravitational/trace" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" @@ -126,3 +128,26 @@ func (s *TerraformSuite) TestImportSAMLConnector() { }, }) } + +func (s *TerraformSuite) TestSAMLConnectorWithEntityDescriptorURL() { + resource.Test(s.T(), resource.TestCase{ + ProtoV6ProviderFactories: s.terraformProviders, + Steps: []resource.TestStep{ + { + Config: s.getFixture("saml_connector_0_create_with_entitydescriptorurl.tf"), + }, + }, + }) +} + +func (s *TerraformSuite) TestSAMLConnectorWithoutEntityDescriptor() { + resource.Test(s.T(), resource.TestCase{ + ProtoV6ProviderFactories: s.terraformProviders, + Steps: []resource.TestStep{ + { + Config: s.getFixture("saml_connector_0_create_without_entitydescriptor.tf"), + ExpectError: regexp.MustCompile("AnyOf 'entity_descriptor, entity_descriptor_url' keys must be present"), + }, + }, + }) +} diff --git a/terraform/tfschema/types_terraform.go b/terraform/tfschema/types_terraform.go index a888595a9..5760783cd 100644 --- a/terraform/tfschema/types_terraform.go +++ b/terraform/tfschema/types_terraform.go @@ -1989,7 +1989,7 @@ func GenSchemaSAMLConnectorV2(ctx context.Context) (github_com_hashicorp_terrafo }, "entity_descriptor": { Description: "EntityDescriptor is XML with descriptor. It can be used to supply configuration parameters in one XML file rather than supplying them in the individual elements.", - Required: true, + Optional: true, Sensitive: true, Type: github_com_hashicorp_terraform_plugin_framework_types.StringType, }, @@ -2050,6 +2050,7 @@ func GenSchemaSAMLConnectorV2(ctx context.Context) (github_com_hashicorp_terrafo }), Description: "Spec is an SAML connector specification.", Required: true, + Validators: []github_com_hashicorp_terraform_plugin_framework_tfsdk.AttributeValidator{UseAnyOfValidator("entity_descriptor", "entity_descriptor_url")}, }, "sub_kind": { Description: "SubKind is an optional resource sub kind, used in some resources.", diff --git a/terraform/tfschema/validators.go b/terraform/tfschema/validators.go index d2231ae13..09f2bb39e 100644 --- a/terraform/tfschema/validators.go +++ b/terraform/tfschema/validators.go @@ -19,6 +19,7 @@ package tfschema import ( "context" fmt "fmt" + "strings" "time" "github.com/hashicorp/terraform-plugin-framework/tfsdk" @@ -163,3 +164,67 @@ OUTER: } } + +// AnyOfValidator validates that at least one of the attributes is set (known and not null) +type AnyOfValidator struct { + Keys []string +} + +// UseAnyOfValidator creates AnyOfValidator +func UseAnyOfValidator(keys ...string) tfsdk.AttributeValidator { + return AnyOfValidator{ + Keys: keys, + } +} + +// Description returns validator description +func (v AnyOfValidator) Description(_ context.Context) string { + return fmt.Sprintf("AnyOf '%s' attributes must be present", strings.Join(v.Keys, ", ")) +} + +// MarkdownDescription returns validator markdown description +func (v AnyOfValidator) MarkdownDescription(_ context.Context) string { + return fmt.Sprintf("AnyOf `%s` attributes must be present", strings.Join(v.Keys, ", ")) +} + +// Validate performs the validation. +func (v AnyOfValidator) Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, resp *tfsdk.ValidateAttributeResponse) { + if req.AttributeConfig == nil { + return + } + + value, ok := req.AttributeConfig.(types.Object) + if !ok { + resp.Diagnostics.AddError( + "AnyOf Object validation error", + fmt.Sprintf("Attribute %v can not be converted to Object", req.AttributePath.String()), + ) + return + } + + if value.Null || value.Unknown { + return + } + + for _, key := range v.Keys { + attr, found := value.Attrs[key] + if found { + tfVal, err := attr.ToTerraformValue(ctx) + if err != nil { + resp.Diagnostics.AddError( + "AnyOf keys validation error", + fmt.Sprintf("Failed to convert ToTerraformValue attribute with key '%s': %v", key, err), + ) + return + } + if !tfVal.IsNull() { + return + } + } + } + + resp.Diagnostics.AddError( + "AnyOf keys validation error", + fmt.Sprintf("AnyOf '%s' keys must be present", strings.Join(v.Keys, ", ")), + ) +} diff --git a/terraform/tfschema/validators_test.go b/terraform/tfschema/validators_test.go new file mode 100644 index 000000000..2726d2f9a --- /dev/null +++ b/terraform/tfschema/validators_test.go @@ -0,0 +1,90 @@ +/* + Copyright 2022 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 tfschema + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/stretchr/testify/require" +) + +func getObjectWithTwoKeys() types.Object { + return types.Object{ + Attrs: map[string]attr.Value{ + "key1": types.String{ + Value: "val1", + Null: false, + Unknown: false, + }, + "key2": types.Int64{ + Value: 12, + Null: false, + Unknown: false, + }, + }, + AttrTypes: map[string]attr.Type{ + "key1": types.StringType, + "key2": types.StringType, + }, + } +} + +func TestAnyOfValidator(t *testing.T) { + type testCase struct { + input types.Object + anyOfValues []string + expectError bool + } + tests := map[string]testCase{ + "one-of-two": { + input: getObjectWithTwoKeys(), + anyOfValues: []string{"key1"}, + expectError: false, + }, + "two-of-two": { + input: getObjectWithTwoKeys(), + anyOfValues: []string{"key1", "key2"}, + expectError: false, + }, + "none-of-two": { + input: getObjectWithTwoKeys(), + anyOfValues: []string{"key3"}, + expectError: true, + }, + } + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + t.Parallel() + ctx := context.Background() + resp := &tfsdk.ValidateAttributeResponse{ + Diagnostics: make(diag.Diagnostics, 0), + } + + req := tfsdk.ValidateAttributeRequest{ + AttributeConfig: test.input, + } + UseAnyOfValidator(test.anyOfValues...).Validate(ctx, req, resp) + require.Equal(t, test.expectError, resp.Diagnostics.HasError()) + }) + } +} From a040039531af25a7f8e2bd1ec7a628e695c59659 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Andr=C3=A9=20Dinis?= Date: Mon, 4 Apr 2022 19:51:16 +0100 Subject: [PATCH 05/18] remove tf plugin framework fork - PR#259 was upstreamed (#481) This fork was created because of this PR hashicorp/terraform-plugin-framework#259 It was upstreamed as part of the 0.6.1 release: https://github.com/hashicorp/terraform-plugin-framework/releases/tag/v0.6.1 We can now remove the replace directive --- go.mod | 10 ++++------ go.sum | 25 +++++++++++++------------ 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/go.mod b/go.mod index ca4fe224c..4d937cd82 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,6 @@ require ( github.com/ghodss/yaml v1.0.0 github.com/go-resty/resty/v2 v2.3.0 github.com/gogo/protobuf v1.3.2 - github.com/golang/protobuf v1.5.0 github.com/google/btree v1.0.1 // indirect github.com/google/go-querystring v1.0.0 github.com/google/uuid v1.2.0 @@ -20,8 +19,8 @@ require ( github.com/gravitational/teleport/api v0.0.0-20220317023058-7bbe6f15c5c8 // tag v9.0.1 github.com/gravitational/trace v1.1.17 github.com/hashicorp/go-version v1.3.0 - github.com/hashicorp/terraform-plugin-framework v0.0.0-00010101000000-000000000000 - github.com/hashicorp/terraform-plugin-go v0.7.1 + github.com/hashicorp/terraform-plugin-framework v0.6.1 + github.com/hashicorp/terraform-plugin-go v0.8.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.10.1 github.com/jonboulle/clockwork v0.2.2 github.com/json-iterator/go v1.1.10 @@ -39,8 +38,8 @@ require ( golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 golang.org/x/tools v0.0.0-20210106214847-113979e3529a // indirect google.golang.org/genproto v0.0.0-20210223151946-22b48be4551b // indirect - google.golang.org/grpc v1.44.0 - google.golang.org/protobuf v1.27.1 // indirect + google.golang.org/grpc v1.45.0 + google.golang.org/protobuf v1.27.1 gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/mail.v2 v2.3.1 gopkg.in/resty.v1 v1.12.0 @@ -49,6 +48,5 @@ require ( replace ( github.com/gogo/protobuf => github.com/gravitational/protobuf v1.3.2-0.20201123192827-2b9fcfaffcbf - github.com/hashicorp/terraform-plugin-framework => github.com/gzigzigzeo/terraform-plugin-framework v0.4.3-0.20220304162136-1d7997102c3b // PR#259 github.com/julienschmidt/httprouter => github.com/rw-access/httprouter v1.3.1-0.20210321233808-98e93175c124 ) diff --git a/go.sum b/go.sum index ae40c2996..df30eb198 100644 --- a/go.sum +++ b/go.sum @@ -206,8 +206,9 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= @@ -262,8 +263,6 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmg github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.10.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/gzigzigzeo/terraform-plugin-framework v0.4.3-0.20220304162136-1d7997102c3b h1:XjYc7KwkHMpQbv8r/20S1HsJd/86TazUkCpjp4eoi5o= -github.com/gzigzigzeo/terraform-plugin-framework v0.4.3-0.20220304162136-1d7997102c3b/go.mod h1:86JHqCS6JvA06Xq2GAYUPMHO41siZm8xINU3cEygHp4= github.com/hashicorp/consul/api v1.7.0/go.mod h1:1NSuaUUkFaJzMasbfq/11wKYWSR67Xn6r2DXKhuDNFg= github.com/hashicorp/consul/sdk v0.6.0/go.mod h1:fY08Y9z5SvJqevyZNy6WWPXiG3KwBPAvlcdx16zZ0fM= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= @@ -282,9 +281,8 @@ github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9 github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-hclog v0.16.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-hclog v1.0.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-hclog v1.1.0 h1:QsGcniKx5/LuX2eYoeL+Np3UKYPNaN7YKpTh29h8rbw= -github.com/hashicorp/go-hclog v1.1.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-hclog v1.2.0 h1:La19f8d7WIlm4ogzNHB0JGqs5AUDAZ2UfCY4sJXcJdM= +github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= @@ -323,12 +321,14 @@ github.com/hashicorp/terraform-exec v0.15.0 h1:cqjh4d8HYNQrDoEmlSGelHmg2DYDh5yay github.com/hashicorp/terraform-exec v0.15.0/go.mod h1:H4IG8ZxanU+NW0ZpDRNsvh9f0ul7C0nHP+rUR/CHs7I= github.com/hashicorp/terraform-json v0.13.0 h1:Li9L+lKD1FO5RVFRM1mMMIBDoUHslOniyEi5CM+FWGY= github.com/hashicorp/terraform-json v0.13.0/go.mod h1:y5OdLBCT+rxbwnpxZs9kGL7R9ExU76+cpdY8zHwoazk= +github.com/hashicorp/terraform-plugin-framework v0.6.1 h1:zUblz+sQ8xEnW0MWWWYRja0mJMabGJig4uJUXSsuY98= +github.com/hashicorp/terraform-plugin-framework v0.6.1/go.mod h1:dgISV1z4CKDmi3uu/YpcvHeCSLtaKgena9Ix27tkKIQ= github.com/hashicorp/terraform-plugin-go v0.5.0/go.mod h1:PAVN26PNGpkkmsvva1qfriae5Arky3xl3NfzKa8XFVM= -github.com/hashicorp/terraform-plugin-go v0.7.1 h1:sZxEVwqkGxoYFu+vs9NI3qR1s0JGjG5DwV/n8PehIPQ= -github.com/hashicorp/terraform-plugin-go v0.7.1/go.mod h1:gP1vMbBqUmOWdZKJzNSAVr2G5G5pkCIK0Uoih0tqcbw= +github.com/hashicorp/terraform-plugin-go v0.8.0 h1:MvY43PcDj9VlBjYifBWCO/6j1wf106xU8d5Tob/WRs0= +github.com/hashicorp/terraform-plugin-go v0.8.0/go.mod h1:E3GuvfX0Pz2Azcl6BegD6t51StXsVZMOYQoGO8mkHM0= github.com/hashicorp/terraform-plugin-log v0.2.0/go.mod h1:E1kJmapEHzqu1x6M++gjvhzM2yMQNXPVWZRCB8sgYjg= -github.com/hashicorp/terraform-plugin-log v0.2.1 h1:hl0G6ctSx7DRTE62VNsPWrq7d+JWy1kjk9ApOFrCq3I= -github.com/hashicorp/terraform-plugin-log v0.2.1/go.mod h1:RW/n0x4dyITmenuirZ1ViPQGP5JQdPTZ4Wwc0rLKi94= +github.com/hashicorp/terraform-plugin-log v0.3.0 h1:NPENNOjaJSVX0f7JJTl4f/2JKRPQ7S2ZN9B4NSqq5kA= +github.com/hashicorp/terraform-plugin-log v0.3.0/go.mod h1:EjueSP/HjlyFAsDqt+okpCPjkT4NDynAe32AeDC4vps= github.com/hashicorp/terraform-plugin-sdk/v2 v2.10.1 h1:B9AocC+dxrCqcf4vVhztIkSkt3gpRjUkEka8AmZWGlQ= github.com/hashicorp/terraform-plugin-sdk/v2 v2.10.1/go.mod h1:FjM9DXWfP0w/AeOtJoSKHBZ01LqmaO6uP4bXhv3fekw= github.com/hashicorp/terraform-registry-address v0.0.0-20210412075316-9b2996cce896 h1:1FGtlkJw87UsTMg5s8jrekrHmUPUJaMcu6ELiVhQrNw= @@ -840,8 +840,8 @@ google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.43.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= -google.golang.org/grpc v1.44.0 h1:weqSxi/TMs1SqFRMHCtBgXRs8k3X39QIDEZ0pRcttUg= -google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.45.0 h1:NEpgUqV3Z+ZjkqMsxMg11IaDrXY4RY6CQukSGK0uI1M= +google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -853,6 +853,7 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= From 3c5935b665bdb5bdb840514849eb6a62e53aada1 Mon Sep 17 00:00:00 2001 From: Walt Date: Mon, 4 Apr 2022 19:40:58 -0700 Subject: [PATCH 06/18] Remove GitLab access plugin (#477) This plugin was never completed or used by any users. We can save ourselves the burden of maintaining it until such time as there is demand. Closes https://github.com/gravitational/teleport-plugins/issues/458 --- Makefile | 13 +- access/GITLAB.md | 9 + access/gitlab/CHANGELOG.md | 0 access/gitlab/Makefile | 60 -- access/gitlab/README.md | 127 ---- access/gitlab/app.go | 730 --------------------- access/gitlab/client.go | 504 --------------- access/gitlab/config.go | 166 ----- access/gitlab/database.go | 161 ----- access/gitlab/fake_gitlab_test.go | 508 --------------- access/gitlab/gitlab_test.go | 961 ---------------------------- access/gitlab/install | 19 - access/gitlab/main.go | 102 --- access/gitlab/plugindata.go | 117 ---- access/gitlab/plugindata_test.go | 84 --- access/gitlab/types.go | 237 ------- access/gitlab/version.go | 25 - access/gitlab/webhook_server.go | 145 ----- charts/access/email/README.md | 2 +- docker/Makefile | 2 +- docker/docker-compose.yml | 15 - docker/ngrok-insecure.yaml.example | 6 +- docker/plugins/teleport-gitlab.toml | 24 - testplan.md | 18 - 24 files changed, 14 insertions(+), 4021 deletions(-) create mode 100644 access/GITLAB.md delete mode 100644 access/gitlab/CHANGELOG.md delete mode 100644 access/gitlab/Makefile delete mode 100644 access/gitlab/README.md delete mode 100644 access/gitlab/app.go delete mode 100644 access/gitlab/client.go delete mode 100644 access/gitlab/config.go delete mode 100644 access/gitlab/database.go delete mode 100644 access/gitlab/fake_gitlab_test.go delete mode 100644 access/gitlab/gitlab_test.go delete mode 100755 access/gitlab/install delete mode 100644 access/gitlab/main.go delete mode 100644 access/gitlab/plugindata.go delete mode 100644 access/gitlab/plugindata_test.go delete mode 100644 access/gitlab/types.go delete mode 100644 access/gitlab/version.go delete mode 100644 access/gitlab/webhook_server.go delete mode 100644 docker/plugins/teleport-gitlab.toml diff --git a/Makefile b/Makefile index 8af49aba3..3306855fa 100644 --- a/Makefile +++ b/Makefile @@ -14,10 +14,6 @@ access-mattermost: access-pagerduty: make -C access/pagerduty -.PHONY: access-gitlab -access-gitlab: - make -C access/gitlab - .PHONY: access-example access-example: go build -o build/access-example ./access/example @@ -34,7 +30,6 @@ docker-build-access-%: # Build all access plugins with docker .PHONY: docker-build-access-plugins docker-build-access-plugins: docker-build-access-email \ - docker-build-access-gitlab \ docker-build-access-jira \ docker-build-access-mattermost \ docker-build-access-pagerduty \ @@ -94,10 +89,6 @@ release/access-mattermost: release/access-pagerduty: make -C access/pagerduty clean release -.PHONY: release/access-gitlab -release/access-gitlab: - make -C access/gitlab clean release - .PHONY: release/access-email release/access-email: make -C access/email clean release @@ -112,10 +103,10 @@ release/event-handler: # Run all releases .PHONY: releases -releases: release/access-slack release/access-jira release/access-mattermost release/access-pagerduty release/access-gitlab release/access-email +releases: release/access-slack release/access-jira release/access-mattermost release/access-pagerduty release/access-email .PHONY: build-all -build-all: access-slack access-jira access-mattermost access-pagerduty access-gitlab access-email terraform event-handler +build-all: access-slack access-jira access-mattermost access-pagerduty access-email terraform event-handler .PHONY: update-version update-version: diff --git a/access/GITLAB.md b/access/GITLAB.md new file mode 100644 index 000000000..e5eb27cc3 --- /dev/null +++ b/access/GITLAB.md @@ -0,0 +1,9 @@ +# GitLab Access Plugin + +We partially developed a GitLab Access Plugin, but there was little-to-no +usage or demand for it. Thus we decided to stop paying the maintenance cost +of the plugin. If you want to recover the work, find the plugin at: + +981ccb982c1e2b09ff98f370019748d783e050d5 + +access/gitlab/* diff --git a/access/gitlab/CHANGELOG.md b/access/gitlab/CHANGELOG.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/access/gitlab/Makefile b/access/gitlab/Makefile deleted file mode 100644 index 5fcd0761d..000000000 --- a/access/gitlab/Makefile +++ /dev/null @@ -1,60 +0,0 @@ -VERSION=9.0.0 -GO_VERSION=1.17.8 - -BUILDDIR ?= build -BINARY = $(BUILDDIR)/teleport-gitlab -GITTAG=v$(VERSION) -GITREF ?= $(shell git describe --dirty --long --tags --match '*gitlab*') -ADDFLAGS ?= -BUILDFLAGS ?= $(ADDFLAGS) -ldflags "-w -s -X main.Gitref=$(GITREF) -X main.Version=$(VERSION)" -CGOFLAG ?= CGO_ENABLED=1 - -OS ?= $(shell go env GOOS) -ARCH ?= $(shell go env GOARCH) -RELEASE_NAME=teleport-access-gitlab -RELEASE=$(RELEASE_NAME)-$(GITTAG)-$(OS)-$(ARCH)-bin - -RELEASE_MESSAGE = "Building with GOOS=$(OS) GOARCH=$(ARCH)." - -DOCKER_NAME=access-plugin-gitlab -DOCKER_IMAGE = 146628656107.dkr.ecr.us-west-2.amazonaws.com/teleport/$(DOCKER_NAME):$(VERSION) -DOCKER_BUILD_ARGS = --build-arg GO_VERSION=${GO_VERSION} --build-arg ACCESS_PLUGIN=gitlab --build-arg GITREF=$(GITREF) - -.PHONY: $(BINARY) -$(BINARY): - GOOS=$(OS) GOARCH=$(ARCH) $(CGOFLAG) go build -o $(BINARY) $(BUILDFLAGS) - -.PHONY: test -test: FLAGS ?= '-race' -test: - GOOS=$(OS) GOARCH=$(ARCH) $(CGOFLAG) go test $(FLAGS) $(ADDFLAGS) - -clean: - @echo "---> Cleaning up build artifacts." - rm -rf $(BUILDDIR) - -go clean -cache - rm -rf $(RELEASE_NAME) - rm -rf *.gz - rm -f gitref.go - -.PHONY: release -release: clean $(BINARY) - @echo "---> $(RELEASE_MESSAGE)" - mkdir $(RELEASE_NAME) - cp -rf $(BINARY) \ - README.md \ - CHANGELOG.md \ - install \ - $(RELEASE_NAME)/ - echo $(GITTAG) > $(RELEASE_NAME)/VERSION - tar -czf $(RELEASE).tar.gz $(RELEASE_NAME) - rm -rf $(RELEASE_NAME)/ - @echo "---> Created $(RELEASE).tar.gz." - -.PHONY: docker-build -docker-build: ## Build docker image with the plugin. - docker build ${DOCKER_BUILD_ARGS} -t ${DOCKER_IMAGE} -f ../Dockerfile ../.. - -.PHONY: docker-push -docker-push: - docker push ${DOCKER_IMAGE} diff --git a/access/gitlab/README.md b/access/gitlab/README.md deleted file mode 100644 index 7752acf57..000000000 --- a/access/gitlab/README.md +++ /dev/null @@ -1,127 +0,0 @@ -# Teleport GitLab Plugin - -The plugin allows teams to setup permissions workflow over their existing or new -GitLab projects. When someone requests new roles in Teleport, an issue will be opened, -and the team members can assign approval or denied label to the issue to approve -or deny the request. - -## Setup - -### Install the plugin - -Get the plugin distribution. - -```bash -$ curl -L https://get.gravitational.com/teleport-access-gitlab-v7.0.2-linux-amd64-bin.tar.gz -$ tar -xzf teleport-access-gitlab-v7.0.2-linux-amd64-bin.tar.gz -$ cd teleport-access-gitlab -$ ./install -``` - -### Set up GitLab project & API token - -1. On GitLab, go "User Settings" -> "Access Tokens". Create a token with api - scope, remember the token. -2. Create a project, get its numeric "Project ID" from "Project Overview" -> - "Details" page. -3. You might want to create the Board with lists: `Teleport: Pending`, - `Teleport: Approved`, and `Teleport: Denied`. The plugin will work if you - just change labels on issues, but with a Board you can just drag the issue - into a status-column you want. - -### Teleport User and Role - -Using Web UI or `tctl` CLI utility, create the role `access-gitlab` and the user `access-gitlab` belonging to the role `access-gitlab`. You may use the following YAML declarations. - -#### Role - -```yaml -kind: role -metadata: - name: access-gitlab -spec: - allow: - rules: - - resources: ['access_request'] - verbs: ['list', 'read', 'update'] -version: v5 -``` - -#### User - -```yaml -kind: user -metadata: - name: access-gitlab -spec: - roles: ['access-gitlab'] -version: v2 -``` - -### Generate the certificate - -For the plugin to connect to Auth Server, it needs an identity file containing TLS/SSH certificates. This can be obtained with tctl: - -```bash -$ tctl auth sign --auth-server=AUTH-SERVER:PORT --format=file --user=access-gitlab --out=/var/lib/teleport/plugins/gitlab/auth_id --ttl=8760h -``` - -Here, `AUTH-SERVER:PORT` could be `localhost:3025`, `your-in-cluster-auth.example.com:3025`, `your-remote-proxy.example.com:3080` or `your-teleport-cloud.teleport.sh:443`. For non-localhost connections, you might want to pass the `--identity=...` option to authenticate yourself to Auth Server. - -### Save configuration file - -By default, configuration file is expected to be at `/etc/teleport-gitlab.toml`. - -```toml -# /etc/teleport-gitlab.toml -[teleport] -# Teleport Auth/Proxy Server address. -# -# Should be port 3025 for Auth Server and 3080 or 443 for Proxy. -# For Teleport Cloud, should be in the form of "your-account.teleport.sh:443". -addr = "example.com:3025" - -# Identity file exported by `tctl auth sign`. -# -identity = "/var/lib/teleport/plugins/gitlab/auth_id" - -[db] -path = "/var/lib/teleport/plugins/gitlab/database" # Path to the database file - -[gitlab] -url = "" # Leave empty if you are using cloud -token = "token" # GitLab API Token -project_id = "1812345" # GitLab Project ID -webhook_secret = "your webhook passphrase" # A secret used to encrypt data we use in webhooks. Basically anything you'd like. - -[http] -public_addr = "example.com" # URL on which webhook server is accessible externally, e.g. [https://]teleport-gitlab.example.com -# listen_addr = ":8081" # Network address in format [addr]:port on which webhook server listens, e.g. 0.0.0.0:443 -https_key_file = "/var/lib/teleport/plugins/gitlab/server.key" # TLS private key -https_cert_file = "/var/lib/teleport/plugins/gitlab/server.crt" # TLS certificate - -[log] -output = "stderr" # Logger output. Could be "stdout", "stderr" or "/var/lib/teleport/gitlab.log" -severity = "INFO" # Logger severity. Could be "INFO", "ERROR", "DEBUG" or "WARN". -``` - -### Run the plugin - -```bash -teleport-gitlab start -``` - -If something bad happens, try to run it with `-d` option i.e. `teleport-gitlab start -d` and attach the stdout output to the issue you are going to create. - -If for some reason you want to disable TLS termination in the plugin and deploy it somewhere else e.g. on some reverse proxy, you may want to run the plugin with `--insecure-no-tls` option. With `--insecure-no-tls` option, plugin's webhook server will talk plain HTTP protocol. - -## Building from source - -To build the plugin from source you need [Go](https://go.dev/) and `make`. - -```bash -git clone https://github.com/gravitational/teleport-plugins.git -cd teleport-plugins/access/gitlab -make -./build/teleport-gitlab start -``` diff --git a/access/gitlab/app.go b/access/gitlab/app.go deleted file mode 100644 index da6943ef9..000000000 --- a/access/gitlab/app.go +++ /dev/null @@ -1,730 +0,0 @@ -/* -Copyright 2020-2021 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" - "net/url" - "time" - - "github.com/jonboulle/clockwork" - "google.golang.org/grpc" - grpcbackoff "google.golang.org/grpc/backoff" - - "github.com/gravitational/teleport-plugins/lib" - "github.com/gravitational/teleport-plugins/lib/backoff" - "github.com/gravitational/teleport-plugins/lib/logger" - "github.com/gravitational/teleport-plugins/lib/watcherjob" - "github.com/gravitational/teleport/api/client" - "github.com/gravitational/teleport/api/client/proto" - "github.com/gravitational/teleport/api/types" - apiutils "github.com/gravitational/teleport/api/utils" - "github.com/gravitational/trace" -) - -const ( - // minServerVersion is the minimal teleport version the plugin supports. - minServerVersion = "6.1.0" - // pluginName is used to tag PluginData and as a Delegator in Audit log. - pluginName = "gitlab" - // grpcBackoffMaxDelay is a maximum time GRPC client waits before reconnection attempt. - grpcBackoffMaxDelay = time.Second * 2 - // initTimeout is used to bound execution time of health check and teleport version check. - initTimeout = time.Second * 10 - // handlerTimeout is used to bound the execution time of watcher event handler. - handlerTimeout = time.Second * 5 - // modifyPluginDataBackoffBase is an initial (minimum) backoff value. - modifyPluginDataBackoffBase = time.Millisecond - // modifyPluginDataBackoffMax is a backoff threshold - modifyPluginDataBackoffMax = time.Second -) - -// App contains global application state. -type App struct { - conf Config - defaultProjectID IntID - - db DB - apiClient *client.Client - gitlab Gitlab - webhookSrv *WebhookServer - mainJob lib.ServiceJob - - *lib.Process -} - -func NewApp(conf Config) (*App, error) { - app := &App{conf: conf} - app.mainJob = lib.NewServiceJob(app.run) - return app, nil -} - -// Run initializes and runs a watcher and a callback server -func (a *App) Run(ctx context.Context) error { - // Initialize the process. - a.Process = lib.NewProcess(ctx) - a.SpawnCriticalJob(a.mainJob) - <-a.Process.Done() - return trace.Wrap(a.mainJob.Err()) -} - -// Err returns the error app finished with. -func (a *App) Err() error { - return trace.Wrap(a.mainJob.Err()) -} - -// WaitReady waits for http and watcher service to start up. -func (a *App) WaitReady(ctx context.Context) (bool, error) { - return a.mainJob.WaitReady(ctx) -} - -// PublicURL returns a webhook base URL. -func (a *App) PublicURL() *url.URL { - if !a.mainJob.IsReady() { - panic("app is not running") - } - return a.webhookSrv.BaseURL() -} - -func (a *App) run(ctx context.Context) error { - var err error - - log := logger.Get(ctx) - log.Infof("Starting Teleport GitLab Plugin %s:%s", Version, Gitref) - - if err = a.init(ctx); err != nil { - return trace.Wrap(err) - } - - httpJob := a.webhookSrv.ServiceJob() - a.SpawnCriticalJob(httpJob) - httpOk, err := httpJob.WaitReady(ctx) - if err != nil { - return trace.Wrap(err) - } - - log.Debug("Setting up the project") - if err = a.setup(ctx, a.defaultProjectID); err != nil { - log.Error("Failed to set up project") - return trace.Wrap(err) - } - log.Debug("GitLab project setup finished ok") - - watcherJob := watcherjob.NewJob( - a.apiClient, - watcherjob.Config{ - Watch: types.Watch{Kinds: []types.WatchKind{types.WatchKind{Kind: types.KindAccessRequest}}}, - EventFuncTimeout: handlerTimeout, - }, - a.onWatcherEvent, - ) - a.SpawnCriticalJob(watcherJob) - watcherOk, err := watcherJob.WaitReady(ctx) - if err != nil { - return trace.Wrap(err) - } - - ok := httpOk && watcherOk - a.mainJob.SetReady(ok) - if ok { - log.Info("Plugin is ready") - } else { - log.Error("Plugin is not ready") - } - - <-httpJob.Done() - <-watcherJob.Done() - - err = a.db.Close() - - return trace.NewAggregate(httpJob.Err(), watcherJob.Err(), err) -} - -func (a *App) init(ctx context.Context) error { - ctx, cancel := context.WithTimeout(ctx, initTimeout) - defer cancel() - log := logger.Get(ctx) - - var ( - err error - pong proto.PingResponse - ) - - bk := grpcbackoff.DefaultConfig - bk.MaxDelay = grpcBackoffMaxDelay - if a.apiClient, err = client.New(ctx, client.Config{ - Addrs: a.conf.Teleport.GetAddrs(), - Credentials: a.conf.Teleport.Credentials(), - DialOpts: []grpc.DialOption{grpc.WithConnectParams(grpc.ConnectParams{Backoff: bk, MinConnectTimeout: initTimeout})}, - }); err != nil { - return trace.Wrap(err) - } - - if pong, err = a.checkTeleportVersion(ctx); err != nil { - return trace.Wrap(err) - } - - webhookSrv, err := NewWebhookServer( - a.conf.HTTP, - a.conf.Gitlab.WebhookSecret, - a.onWebhookEvent, - ) - if err != nil { - return trace.Wrap(err) - } - err = webhookSrv.EnsureCert() - if err != nil { - return trace.Wrap(err) - } - - a.webhookSrv = webhookSrv - - var webProxyAddr string - if pong.ServerFeatures.AdvancedAccessWorkflows { - webProxyAddr = pong.ProxyPublicAddr - } - a.gitlab, err = NewGitlabClient(a.conf.Gitlab, pong.ClusterName, webProxyAddr, webhookSrv) - if err != nil { - return trace.Wrap(err) - } - - log.Debug("Starting GitLab API health check...") - a.defaultProjectID, err = a.gitlab.HealthCheck(ctx, a.conf.Gitlab.ProjectID) - if err != nil { - return trace.Wrap(err, "api health check failed") - } - log.Debug("GitLab API health check finished ok") - - log.Debug("Opening the database...") - a.db, err = OpenDB(a.conf.DB.Path) - if err != nil { - return trace.Wrap(err, "failed to open the database") - } - - return nil -} - -func (a *App) checkTeleportVersion(ctx context.Context) (proto.PingResponse, error) { - log := logger.Get(ctx) - log.Debug("Checking Teleport server version") - pong, err := a.apiClient.Ping(ctx) - if err != nil { - if trace.IsNotImplemented(err) { - return pong, trace.Wrap(err, "server version must be at least %s", minServerVersion) - } - log.Error("Unable to get Teleport server version") - return pong, trace.Wrap(err) - } - err = lib.AssertServerVersion(pong, minServerVersion) - return pong, trace.Wrap(err) -} - -func (a *App) setup(ctx context.Context, projectID IntID) error { - return a.db.UpdateSettings(projectID, func(settings SettingsBucket) (err error) { - webhookID := settings.HookID() - if webhookID, err = a.gitlab.SetupProjectHook(ctx, projectID, webhookID); err != nil { - return - } - if err = settings.SetHookID(webhookID); err != nil { - return - } - - labels := settings.GetLabels( - "pending", - "approved", - "denied", - "expired", - ) - if err = a.gitlab.SetupLabels(ctx, projectID, labels); err != nil { - return - } - if err = settings.SetLabels(a.gitlab.labels); err != nil { - return - } - return - }) -} - -func (a *App) onWatcherEvent(ctx context.Context, event types.Event) error { - if kind := event.Resource.GetKind(); kind != types.KindAccessRequest { - return trace.Errorf("unexpected kind %s", kind) - } - op := event.Type - reqID := event.Resource.GetName() - ctx, _ = logger.WithField(ctx, "request_id", reqID) - - switch op { - case types.OpPut: - ctx, _ = logger.WithField(ctx, "request_op", "put") - req, ok := event.Resource.(types.AccessRequest) - if !ok { - return trace.Errorf("unexpected resource type %T", event.Resource) - } - ctx, log := logger.WithField(ctx, "request_state", req.GetState().String()) - log.Debug("Processing watcher event") - - var err error - switch { - case req.GetState().IsPending(): - err = a.onPendingRequest(ctx, req) - case req.GetState().IsApproved(): - err = a.onResolvedRequest(ctx, req) - case req.GetState().IsDenied(): - err = a.onResolvedRequest(ctx, req) - default: - log.WithField("event", event).Warn("Unknown request state") - return nil - } - - if err != nil { - log.WithError(err).Error("Failed to process request") - return trace.Wrap(err) - } - - return nil - case types.OpDelete: - ctx, log := logger.WithField(ctx, "request_op", "delete") - - if err := a.onDeletedRequest(ctx, reqID); err != nil { - log.WithError(err).Errorf("Failed to process deleted request") - return trace.Wrap(err) - } - return nil - default: - return trace.BadParameter("unexpected event operation %s", op) - } -} - -func (a *App) onWebhookEvent(ctx context.Context, hook Webhook) error { - // Not an issue event - event, ok := hook.Event.(IssueEvent) - if !ok { - return nil - } - - eventAction := event.ObjectAttributes.Action - // Non-update action - if eventAction != "update" { - return nil - } - // No labels changed - if event.Changes.Labels == nil { - return nil - } - - projectID := event.ObjectAttributes.ProjectID - issueID := event.ObjectAttributes.ID - issueIID := event.ObjectAttributes.IID - - ctx, log := logger.WithFields(ctx, logger.Fields{ - "gitlab_issue_id": issueID, - "gitlab_issue_iid": issueIID, - "gitlab_project_id": projectID, - }) - log.Debugf("Processing incoming webhook action %q, labels are changed", eventAction) - - var action ActionID - - for _, label := range event.Changes.Labels.Diff() { - action = LabelName(label.Title).ToAction() - if action != NoAction { - break - } - } - if action == NoAction { - log.Debug("No approved/denied labels set, ignoring") - return nil - } - - var reqID string - err := a.db.ViewIssues(projectID, func(issues IssuesBucket) error { - reqID = issues.GetRequestID(issueIID) - return nil - }) - - ctx, log = logger.WithField(ctx, "request_id", reqID) - - if trace.Unwrap(err) == ErrNoBucket || reqID == "" { - log.WithError(err).Warning("Failed to find an issue in database") - reqID = event.ObjectAttributes.ParseDescriptionRequestID() - if reqID == "" { - // Ignore the issue, probably it wasn't created by us at all. - return nil - } - log.Warning("Request ID was parsed from issue description") - } else if err != nil { - return trace.Wrap(err) - } - - reqs, err := a.apiClient.GetAccessRequests(ctx, types.AccessRequestFilter{ID: reqID}) - if err != nil { - return trace.Wrap(err) - } - - var req types.AccessRequest - if len(reqs) > 0 { - req = reqs[0] - } - - // Validate plugin data that it's matching with the webhook information - pluginData, err := a.getPluginData(ctx, reqID) - if err != nil { - return trace.Wrap(err) - } - if pluginData.IssueID == 0 || pluginData.IssueIID == 0 || pluginData.ProjectID == 0 { - return trace.Errorf("plugin data is blank") - } - if pluginData.IssueID != issueID { - log.WithField("plugin_data_issue_id", pluginData.IssueID). - Debug("plugin_data.issue_id does not match event.issue_id") - return trace.Errorf("issue_id from request's plugin_data does not match") - } - if pluginData.IssueIID != issueIID { - log.WithField("plugin_data_issue_iid", pluginData.IssueIID). - Debug("plugin_data.issue_iid does not match event.issue_iid") - return trace.Errorf("issue_iid from request's plugin_data does not match") - } - if pluginData.ProjectID != projectID { - log.WithField("plugin_data_project_id", pluginData.ProjectID). - Debug("plugin_data.project_id does not match event.project_id") - return trace.Errorf("project_id from request's plugin_data does not match") - } - - if req == nil { - return trace.Wrap(a.resolveIssue(ctx, reqID, Resolution{Tag: ResolvedExpired})) - } - - var resolution Resolution - state := req.GetState() - switch { - case state.IsPending(): - switch action { - case ApproveAction: - resolution.Tag = ResolvedApproved - case DenyAction: - resolution.Tag = ResolvedDenied - default: - return trace.BadParameter("unknown action: %v", action) - } - ctx, _ := logger.WithFields(ctx, logger.Fields{ - "gitlab_user_name": event.User.Name, - "gitlab_user_username": event.User.Username, - "gitlab_user_email": event.User.Email, - }) - if err := a.resolveRequest(ctx, reqID, event.User.Email, resolution); err != nil { - return trace.Wrap(err) - } - case state.IsApproved(): - resolution.Tag = ResolvedApproved - case state.IsDenied(): - resolution.Tag = ResolvedDenied - default: - return trace.BadParameter("unknown request state %v (%s)", state, state) - } - - return trace.Wrap(a.resolveIssue(ctx, reqID, resolution)) -} - -func (a *App) onPendingRequest(ctx context.Context, req types.AccessRequest) error { - reqID := req.GetName() - reqData := RequestData{User: req.GetUser(), Roles: req.GetRoles(), Created: req.GetCreationTime()} - - // Create plugin data if it didn't exist before. - isNew, err := a.modifyPluginData(ctx, reqID, func(existing *PluginData) (PluginData, bool) { - if existing != nil { - return PluginData{}, false - } - return PluginData{RequestData: reqData}, true - }) - if err != nil { - return trace.Wrap(err) - } - - if isNew { - if err := a.createIssue(ctx, a.defaultProjectID, reqID, reqData); err != nil { - return trace.Wrap(err) - } - } - - if reqReviews := req.GetReviews(); len(reqReviews) > 0 { - if err = a.postReviewComments(ctx, reqID, reqReviews); err != nil { - return trace.Wrap(err) - } - } - - return trace.Wrap(err) -} - -func (a *App) onResolvedRequest(ctx context.Context, req types.AccessRequest) error { - err1 := trace.Wrap(a.postReviewComments(ctx, req.GetName(), req.GetReviews())) - - resolution := Resolution{Reason: req.GetResolveReason()} - switch req.GetState() { - case types.RequestState_APPROVED: - resolution.Tag = ResolvedApproved - case types.RequestState_DENIED: - resolution.Tag = ResolvedDenied - } - err2 := trace.Wrap(a.resolveIssue(ctx, req.GetName(), resolution)) - - return trace.NewAggregate(err1, err2) -} - -func (a *App) onDeletedRequest(ctx context.Context, reqID string) error { - return a.resolveIssue(ctx, reqID, Resolution{Tag: ResolvedExpired}) -} - -// createIssue posts a GitLab issue with request information. -func (a *App) createIssue(ctx context.Context, projectID IntID, reqID string, reqData RequestData) error { - ctx, _ = logger.WithField(ctx, "gitlab_project_id", projectID) - - data, err := a.gitlab.CreateIssue(ctx, projectID, reqID, reqData) - if err != nil { - return trace.Wrap(err) - } - - issueIID := data.IssueIID - - ctx, log := logger.WithField(ctx, "gitlab_issue_iid", issueIID) - log.Info("GitLab issue created") - - // Save GitLab issue to request id mapping into file database. - err1 := a.db.UpdateIssues(data.ProjectID, func(issues IssuesBucket) error { - return issues.SetRequestID(issueIID, reqID) - }) - if err1 != nil { - return trace.Wrap(err1) - } - - // Save GitLab issue info in plugin data. - _, err2 := a.modifyPluginData(ctx, reqID, func(existing *PluginData) (PluginData, bool) { - var pluginData PluginData - if existing != nil { - pluginData = *existing - } else { - // It must be impossible but lets handle it just in case. - pluginData = PluginData{RequestData: reqData} - } - pluginData.GitlabData = data - return pluginData, true - }) - - return trace.NewAggregate(err1, err2) -} - -// postReviewComments posts issue comments about new reviews appeared for request. -func (a *App) postReviewComments(ctx context.Context, reqID string, reqReviews []types.AccessReview) error { - var oldCount int - var data GitlabData - - // Increase the review counter in plugin data. - ok, err := a.modifyPluginData(ctx, reqID, func(existing *PluginData) (PluginData, bool) { - // If plugin data is missing issue identification info, we cannot do anything. - if existing == nil { - data = GitlabData{} - return PluginData{}, false - } - - data = existing.GitlabData - // If plugin data has blank issue identification info, we cannot do anything. - if data.ProjectID == 0 || data.IssueIID == 0 { - return PluginData{}, false - } - - count := len(reqReviews) - // If reviews counter is at least the same as it was before, we shouldn't do anything. - if oldCount = existing.ReviewsCount; oldCount >= count { - return PluginData{}, false - } - pluginData := *existing - pluginData.ReviewsCount = count - return pluginData, true - }) - if err != nil { - return trace.Wrap(err) - } - if !ok { - if data.ProjectID == 0 || data.IssueIID == 0 { - logger.Get(ctx).Debug("Failed to post the comment: plugin data is blank") - } - return nil - } - ctx, _ = logger.WithFields(ctx, logger.Fields{ - "gitlab_project_id": data.ProjectID, - "gitlab_issue_iid": data.IssueIID, - }) - - slice := reqReviews[oldCount:] - if len(slice) == 0 { - return nil - } - - errors := make([]error, 0, len(slice)) - for _, review := range slice { - if err := a.gitlab.PostReviewComment(ctx, data.ProjectID, data.IssueIID, review); err != nil { - errors = append(errors, err) - } - } - return trace.NewAggregate(errors...) -} - -// resolveRequest sets an access request state. -func (a *App) resolveRequest(ctx context.Context, reqID string, userEmail string, resolution Resolution) error { - params := types.AccessRequestUpdate{RequestID: reqID} - - switch resolution.Tag { - case ResolvedApproved: - params.State = types.RequestState_APPROVED - case ResolvedDenied: - params.State = types.RequestState_DENIED - default: - return trace.BadParameter("unknown resolution tag %v", resolution.Tag) - } - - delegator := fmt.Sprintf("%s:%s", pluginName, userEmail) - - if err := a.apiClient.SetAccessRequestState(apiutils.WithDelegator(ctx, delegator), params); err != nil { - return trace.Wrap(err) - } - - logger.Get(ctx).Infof("GitLab user %s the request", resolution.Tag) - return nil -} - -// resolveIssue closes the issue to some final state. -func (a *App) resolveIssue(ctx context.Context, reqID string, resolution Resolution) error { - var data GitlabData - - // Save request resolution info in plugin data. - ok, err := a.modifyPluginData(ctx, reqID, func(existing *PluginData) (PluginData, bool) { - // If plugin data is missing issue identification info, we cannot do anything. - if existing == nil { - data = GitlabData{} - return PluginData{}, false - } - - data = existing.GitlabData - // If plugin data has blank issue identification info, we cannot do anything. - if data.ProjectID == 0 || data.IssueIID == 0 { - return PluginData{}, false - } - - // If resolution field is not empty then we already resolved the issue before. In this case we just quit. - if existing.RequestData.Resolution.Tag != Unresolved { - return PluginData{}, false - } - - // Mark issue as resolved. - pluginData := *existing - pluginData.Resolution = resolution - return pluginData, true - }) - if err != nil { - return trace.Wrap(err) - } - if !ok { - if data.ProjectID == 0 || data.IssueIID == 0 { - logger.Get(ctx).Debug("Failed to resolve the issue: plugin data is blank") - } else { - logger.Get(ctx).Debug("Issue was already resolved by us") - } - - // Either plugin data is missing or issue is already resolved by us, just quit. - return nil - } - - ctx, log := logger.WithFields(ctx, logger.Fields{ - "gitlab_project_id": data.ProjectID, - "gitlab_issue_iid": data.IssueIID, - }) - if err := a.gitlab.ResolveIssue(ctx, data.ProjectID, data.IssueIID, resolution); err != nil { - return trace.Wrap(err) - } - log.Info("Successfully resolved the issue") - - return nil -} - -// modifyPluginData performs a compare-and-swap update of access request's plugin data. -// Callback function parameter is nil if plugin data hasn't been created yet. -// Otherwise, callback function parameter is a pointer to current plugin data contents. -// Callback function return value is an updated plugin data contents plus the boolean flag -// indicating whether it should be written or not. -// Note that callback function fn might be called more than once due to retry mechanism baked in -// so make sure that the function is "pure" i.e. it doesn't interact with the outside world: -// it doesn't perform any sort of I/O operations so even things like Go channels must be avoided. -// Indeed, this limitation is not that ultimate at least if you know what you're doing. -func (a *App) modifyPluginData(ctx context.Context, reqID string, fn func(data *PluginData) (PluginData, bool)) (bool, error) { - backoff := backoff.NewDecorr(modifyPluginDataBackoffBase, modifyPluginDataBackoffMax, clockwork.NewRealClock()) - for { - oldData, err := a.getPluginData(ctx, reqID) - if err != nil && !trace.IsNotFound(err) { - return false, trace.Wrap(err) - } - newData, ok := fn(oldData) - if !ok { - return false, nil - } - var expectData PluginData - if oldData != nil { - expectData = *oldData - } - err = trace.Wrap(a.updatePluginData(ctx, reqID, newData, expectData)) - if err == nil { - return true, nil - } - if !trace.IsCompareFailed(err) { - return false, trace.Wrap(err) - } - if err := backoff.Do(ctx); err != nil { - return false, trace.Wrap(err) - } - } -} - -// getPluginData loads a plugin data for a given access request. It returns nil if it's not found. -func (a *App) getPluginData(ctx context.Context, reqID string) (*PluginData, error) { - dataMaps, err := a.apiClient.GetPluginData(ctx, types.PluginDataFilter{ - Kind: types.KindAccessRequest, - Resource: reqID, - Plugin: pluginName, - }) - if err != nil { - return nil, trace.Wrap(err) - } - if len(dataMaps) == 0 { - return nil, trace.NotFound("plugin data not found") - } - entry := dataMaps[0].Entries()[pluginName] - if entry == nil { - return nil, trace.NotFound("plugin data entry not found") - } - data := DecodePluginData(entry.Data) - return &data, nil -} - -// updatePluginData updates an existing plugin data or sets a new one if it didn't exist. -func (a *App) updatePluginData(ctx context.Context, reqID string, data PluginData, expectData PluginData) error { - return a.apiClient.UpdatePluginData(ctx, types.PluginDataUpdateParams{ - Kind: types.KindAccessRequest, - Resource: reqID, - Plugin: pluginName, - Set: EncodePluginData(data), - Expect: EncodePluginData(expectData), - }) -} diff --git a/access/gitlab/client.go b/access/gitlab/client.go deleted file mode 100644 index e17a40d49..000000000 --- a/access/gitlab/client.go +++ /dev/null @@ -1,504 +0,0 @@ -/* -Copyright 2020-2021 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" - "net/http" - "net/url" - "regexp" - "strings" - "text/template" - "time" - - "gopkg.in/resty.v1" - - "github.com/gravitational/teleport-plugins/lib" - "github.com/gravitational/teleport-plugins/lib/logger" - "github.com/gravitational/teleport/api/types" - "github.com/gravitational/trace" -) - -const ( - gitlabMaxConns = 100 - gitlabHTTPTimeout = 10 * time.Second -) - -type Gitlab struct { - client *resty.Client - server *WebhookServer - webhookSecret string - baseURL *url.URL - apiToken string - - clusterName string - webProxyURL *url.URL - labels map[string]string -} - -var nextLinkHeaderRegex = regexp.MustCompile(`<([^>]+)>;\s+rel="next"`) - -var descriptionTemplate = template.Must(template.New("description").Parse( - `{{.User}} requested permissions for roles {{range $index, $element := .Roles}}{{if $index}}, {{end}}**{{ . }}**{{end}} on Teleport at **{{.Created.Format .TimeFormat}}**. To approve or deny the request, please assign a corresponding label and close the issue{{if .RequestLink}} or proceed to {{.RequestLink}}{{end}}. - -{{if .RequestReason}}Reason: **{{.RequestReason}}**.{{end}} - -Request ID is ` + "`{{.ID}}`.", -)) -var reviewCommentTemplate = template.Must(template.New("review comment").Parse( - `**{{.Author}}** reviewed the request at **{{.Created.Format .TimeFormat}}**. - -Resolution: **{{.ProposedState}}**. - -{{if .Reason}}Reason: {{.Reason}}.{{end}}`, -)) -var resolutionCommentTemplate = template.Must(template.New("resolution comment").Parse( - `Access request has been {{.Resolution}} - -{{if .ResolveReason}}Reason: {{.ResolveReason}}{{end}}`, -)) - -// NewGitlabClient builds a new GitLab client. -func NewGitlabClient(conf GitlabConfig, clusterName, webProxyAddr string, server *WebhookServer) (Gitlab, error) { - var ( - webProxyURL *url.URL - err error - ) - if webProxyAddr != "" { - if webProxyURL, err = lib.AddrToURL(webProxyAddr); err != nil { - return Gitlab{}, trace.Wrap(err) - } - } - - client := resty.NewWithClient(&http.Client{ - Timeout: gitlabHTTPTimeout, - Transport: &http.Transport{ - MaxConnsPerHost: gitlabMaxConns, - MaxIdleConnsPerHost: gitlabMaxConns, - }, - }) - - var baseURL *url.URL - if urlStr := conf.URL; urlStr != "" { - baseURL, err = url.Parse(urlStr) - if err != nil { - return Gitlab{}, trace.Wrap(err) - } - } else { - baseURL = &url.URL{ - Scheme: "https", - Host: "gitlab.com", - } - } - return Gitlab{ - client: client, - server: server, - baseURL: baseURL, - clusterName: clusterName, - webProxyURL: webProxyURL, - apiToken: conf.Token, - webhookSecret: conf.WebhookSecret, - labels: map[string]string{}, - }, nil -} - -func (g Gitlab) NewRequest(ctx context.Context) *resty.Request { - return g.client.R(). - SetContext(ctx). - SetError(&ErrorResult{}). - SetHeader("Accept", "application/json"). - SetHeader("PRIVATE-TOKEN", g.apiToken) -} - -func (g Gitlab) APIV4URL(args ...interface{}) string { - args = append([]interface{}{"api", "v4"}, args...) - url := *g.baseURL - url.Path = lib.BuildURLPath(args...) - return url.String() -} - -// HealthCheck checks that project is accessible by API. -func (g Gitlab) HealthCheck(ctx context.Context, projectIDOrPath string) (IntID, error) { - var project Project - resp, err := g.NewRequest(ctx). - SetResult(&project). - Get(g.APIV4URL("projects", projectIDOrPath)) - if err != nil { - return 0, trace.Wrap(err) - } - if resp.IsError() { - if contentType := resp.Header().Get("Content-Type"); contentType != "application/json" { - return 0, trace.Errorf("wrong content type %s", contentType) - } - if code := resp.StatusCode(); code == http.StatusUnauthorized { - return 0, trace.Errorf("got %v from API endpoint, perhaps GitLab credentials are not configured well", code) - } - return 0, responseError(resp) - } - if project.ID == 0 { - return 0, trace.Errorf("bad response from GitLab API") - } - return project.ID, nil -} - -func (g Gitlab) listPages(ctx context.Context, url string, result interface{}, fn func(interface{}) bool) error { - req := g.NewRequest(ctx) - req.SetQueryParams(map[string]string{ - "order_by": "id", - "sort": "asc", - }) - - for { - req.SetResult(result) - resp, err := req.Get(url) - if err != nil { - return trace.Wrap(err) - } - if resp.IsError() { - return trace.Wrap(responseError(resp)) - } - - if !fn(resp.Result()) { - break - } - - submatches := nextLinkHeaderRegex.FindStringSubmatch(resp.Header().Get("link")) - if len(submatches) > 1 { - req = g.NewRequest(ctx) - url = submatches[1] - } else { - break - } - } - return nil -} - -func (g Gitlab) SetupProjectHook(ctx context.Context, projectID, existingHookID IntID) (IntID, error) { - var err error - url := g.server.WebhookURL() - if existingHookID == 0 { - existingHookID, err = g.findProjectHook(ctx, projectID, url) - if err != nil { - return 0, trace.Wrap(err) - } - if existingHookID != 0 { - return existingHookID, nil - } - return g.createProjectHook(ctx, projectID, url) - } - resp, err := g.NewRequest(ctx). - SetBody(HookParams{ - URL: url, - Token: g.webhookSecret, - EnableIssueEvents: true, - }). - Put(g.APIV4URL("projects", projectID, "hooks", existingHookID)) - if err != nil { - return 0, trace.Wrap(err) - } - if resp.IsError() { - if resp.StatusCode() == http.StatusNotFound { - return g.createProjectHook(ctx, projectID, url) - } - return 0, responseError(resp) - } - return existingHookID, nil -} - -func (g Gitlab) findProjectHook(ctx context.Context, projectID IntID, webhookURL string) (IntID, error) { - var result IntID - err := g.listPages(ctx, g.APIV4URL("projects", projectID, "hooks"), []ProjectHook(nil), func(page interface{}) bool { - for _, hook := range *page.(*[]ProjectHook) { - if hook.URL == webhookURL { - result = hook.ID - return false - } - } - return true - }) - return result, trace.Wrap(err) -} - -func (g Gitlab) createProjectHook(ctx context.Context, projectID IntID, url string) (IntID, error) { - var result struct { - ID IntID `json:"id"` - } - resp, err := g.NewRequest(ctx). - SetBody(HookParams{ - URL: url, - Token: g.webhookSecret, - EnableIssueEvents: true, - }). - SetResult(&result). - Post(g.APIV4URL("projects", projectID, "hooks")) - if err != nil { - return 0, trace.Wrap(err) - } - if resp.IsError() { - return 0, trace.Wrap(responseError(resp)) - } - return result.ID, nil -} - -func (g *Gitlab) SetupLabels(ctx context.Context, projectID IntID, existingLabels map[string]string) error { - existingKeys := make(map[string]string) - for key, name := range existingLabels { - if name != "" { - existingKeys[name] = key - } - } - err := g.listPages(ctx, g.APIV4URL("projects", projectID, "labels"), []Label(nil), func(page interface{}) bool { - for _, label := range *page.(*[]Label) { - if key := existingKeys[label.Name]; key != "" { - g.labels[key] = label.Name - } else if key := LabelName(label.Name).Reduced(); key != "" && g.labels[key] == "" { - g.labels[key] = label.Name - } - } - return true - }) - if err != nil { - return trace.Wrap(err) - } - for key := range existingLabels { - if name := g.labels[key]; name == "" { - name, err := g.createLabel(ctx, projectID, key) - if err != nil { - return trace.Wrap(err) - } - g.labels[key] = name - } else { - g.labels[key] = name - } - } - return nil -} - -func (g Gitlab) createLabel(ctx context.Context, projectID IntID, key string) (string, error) { - log := logger.Get(ctx) - name := fmt.Sprintf("Teleport: %s", strings.Title(key)) - log.Debugf("Trying to create a label %q", name) - var label Label - resp, err := g.NewRequest(ctx). - SetBody(LabelParams{ - Name: name, - Color: defaultLabelColor(key), - }). - SetResult(&label). - Post(g.APIV4URL("projects", projectID, "labels")) - if err != nil { - return "", trace.Wrap(err) - } - if resp.IsError() { - if resp.Error().(*ErrorResult).Message == "Label already exists" { - log.Debugf("Label %q already exists", name) - // Race condition here though normally it must not happen. - return name, nil - - } - return "", trace.Wrap(responseError(resp)) - } - return label.Name, nil -} - -func defaultLabelColor(key string) string { - switch key { - case "pending": - return "#FFECDB" - case "approved": - return "#428BCA" - case "denied": - return "#D9534F" - case "expired": - return "#7F8C8D" - default: - return "" - } -} - -func (g Gitlab) CreateIssue(ctx context.Context, projectID IntID, reqID string, reqData RequestData) (GitlabData, error) { - description, err := g.buildIssueDescription(reqID, reqData) - if err != nil { - return GitlabData{}, trace.Wrap(err) - } - var result struct { - ID IntID `json:"id"` - IID IntID `json:"iid"` - ProjectID IntID `json:"project_id"` - } - resp, err := g.NewRequest(ctx). - SetBody(IssueParams{ - Title: fmt.Sprintf("Access request from %s", reqData.User), - Description: description, - Labels: g.labels["pending"], - }). - SetResult(&result). - Post(g.APIV4URL("projects", projectID, "issues")) - if err != nil { - return GitlabData{}, trace.Wrap(err) - } - if resp.IsError() { - return GitlabData{}, trace.Wrap(responseError(resp)) - } - return GitlabData{ - IssueID: result.ID, - IssueIID: result.IID, - ProjectID: result.ProjectID, - }, nil -} - -func (g Gitlab) buildIssueDescription(reqID string, reqData RequestData) (string, error) { - var requestLink string - if g.webProxyURL != nil { - reqURL := *g.webProxyURL - reqURL.Path = lib.BuildURLPath("web", "requests", reqID) - requestLink = reqURL.String() - } - - var builder strings.Builder - err := descriptionTemplate.Execute(&builder, struct { - ID string - TimeFormat string - RequestLink string - RequestData - }{ - reqID, - time.RFC822, - requestLink, - reqData, - }) - if err != nil { - return "", trace.Wrap(err) - } - return builder.String(), nil -} - -// GetIssue loads issue info. -func (g Gitlab) GetIssue(ctx context.Context, projectID, issueIID IntID) (Issue, error) { - var issue Issue - resp, err := g.NewRequest(ctx). - SetResult(&issue). - Get(g.APIV4URL("projects", projectID, "issues", issueIID)) - if err != nil { - return Issue{}, trace.Wrap(err) - } - if resp.IsError() { - return Issue{}, trace.Wrap(responseError(resp)) - } - return issue, nil -} - -// ResolveIssue adds a resolution comment to the issue and closes it. -func (g Gitlab) ResolveIssue(ctx context.Context, projectID, issueIID IntID, resolution Resolution) error { - // Try to add a comment. - err1 := trace.Wrap(g.PostResolutionComment(ctx, projectID, issueIID, resolution)) - - // Try to close the issue. - err2 := trace.Wrap(g.CloseIssue(ctx, projectID, issueIID, resolution)) - - return trace.NewAggregate(err1, err2) -} - -// CloseIssue sets an issue e.g. "approved", "denied" or "expired" and closes it. -func (g Gitlab) CloseIssue(ctx context.Context, projectID, issueIID IntID, resolution Resolution) error { - params := IssueParams{ - StateEvent: "close", - RemoveLabels: g.labels["pending"], - AddLabels: g.labels[string(resolution.Tag)], - } - resp, err := g.NewRequest(ctx). - SetBody(params). - Put(g.APIV4URL("projects", projectID, "issues", issueIID)) - if err != nil { - return trace.Wrap(err) - } - if resp.IsError() { - return trace.Wrap(responseError(resp)) - } - - logger.Get(ctx).Debug("Successfully closed the issue") - return nil -} - -// PostReviewComment posts an issue comment about access review added to a request. -func (g Gitlab) PostReviewComment(ctx context.Context, projectID, issueIID IntID, review types.AccessReview) error { - var builder strings.Builder - err := reviewCommentTemplate.Execute(&builder, struct { - types.AccessReview - ProposedState string - TimeFormat string - }{ - review, - review.ProposedState.String(), - time.RFC822, - }) - if err != nil { - return trace.Wrap(err) - } - resp, err := g.NewRequest(ctx). - SetBody(NoteParams{Body: builder.String()}). - Post(g.APIV4URL("projects", projectID, "issues", issueIID, "notes")) - if err != nil { - return trace.Wrap(err) - } - if resp.IsError() { - return trace.Wrap(responseError(resp)) - } - - logger.Get(ctx).Debug("Successfully posted a review comment to the issue") - return nil -} - -// PostResolutionComment posts an issue comment about access review added to a request. -func (g Gitlab) PostResolutionComment(ctx context.Context, projectID, issueIID IntID, resolution Resolution) error { - var builder strings.Builder - err := resolutionCommentTemplate.Execute(&builder, struct { - Resolution string - ResolveReason string - }{ - string(resolution.Tag), - resolution.Reason, - }) - if err != nil { - return trace.Wrap(err) - } - resp, err := g.NewRequest(ctx). - SetBody(NoteParams{Body: builder.String()}). - Post(g.APIV4URL("projects", projectID, "issues", issueIID, "notes")) - if err != nil { - return trace.Wrap(err) - } - if resp.IsError() { - return trace.Wrap(responseError(resp)) - } - - logger.Get(ctx).Debug("Successfully posted a resolution comment to the issue") - return nil -} - -func responseError(resp *resty.Response) error { - result := resp.Error().(*ErrorResult) - err := fmt.Sprintf("http error code=%v", resp.StatusCode()) - if result.Error != "" { - err += fmt.Sprintf(", error=%q", result.Error) - } - if result.Message != nil { - err += fmt.Sprintf(", message=%v", result.Message) - } - return trace.Errorf(err) -} diff --git a/access/gitlab/config.go b/access/gitlab/config.go deleted file mode 100644 index c0e2a2d85..000000000 --- a/access/gitlab/config.go +++ /dev/null @@ -1,166 +0,0 @@ -/* -Copyright 2020-2021 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 ( - "crypto/tls" - "crypto/x509" - "io/ioutil" - "os" - "path" - "strings" - - "github.com/gravitational/teleport-plugins/lib" - "github.com/gravitational/teleport-plugins/lib/logger" - "github.com/gravitational/trace" - "github.com/pelletier/go-toml" -) - -type Config struct { - Teleport lib.TeleportConfig `toml:"teleport"` - DB struct { - Path string `toml:"path"` - } `toml:"db"` - Gitlab GitlabConfig `toml:"gitlab"` - HTTP lib.HTTPConfig `toml:"http"` - Log logger.Config `toml:"log"` -} - -type GitlabConfig struct { - URL string `toml:"url"` - Token string `toml:"token"` - ProjectID string `toml:"project_id"` - WebhookSecret string `toml:"webhook_secret"` -} - -const exampleConfig = `# example teleport-gitlab configuration TOML file -[teleport] -# Teleport Auth/Proxy Server address. -# -# Should be port 3025 for Auth Server and 3080 or 443 for Proxy. -# For Teleport Cloud, should be in the form "your-account.teleport.sh:443". -addr = "example.com:3025" - -# Credentials. -# -# When using --format=file: -# identity = "/var/lib/teleport/plugins/gitlab/auth_id" # Identity file -# -# When using --format=tls: -# client_key = "/var/lib/teleport/plugins/gitlab/auth.key" # Teleport TLS secret key -# client_crt = "/var/lib/teleport/plugins/gitlab/auth.crt" # Teleport TLS certificate -# root_cas = "/var/lib/teleport/plugins/gitlab/auth.cas" # Teleport CA certs - -[db] -path = "/var/lib/teleport/plugins/gitlab/database" # Path to the database file - -[gitlab] -url = "" # Leave empty if you are using cloud -token = "token" # GitLab API Token -project_id = "1812345" # GitLab Project ID -webhook_secret = "your webhook passphrase" # A secret used to encrypt data we use in webhooks. Basically anything you'd like. - -[http] -public_addr = "example.com" # URL on which callback server is accessible externally, e.g. [https://]teleport-proxy.example.com -# listen_addr = ":8081" # Network address in format [addr]:port on which callback server listens, e.g. 0.0.0.0:8081 -https_key_file = "/var/lib/teleport/webproxy_key.pem" # TLS private key -https_cert_file = "/var/lib/teleport/webproxy_cert.pem" # TLS certificate - -[log] -output = "stderr" # Logger output. Could be "stdout", "stderr" or "/var/lib/teleport/gitlab.log" -severity = "INFO" # Logger severity. Could be "INFO", "ERROR", "DEBUG" or "WARN". -` - -func LoadConfig(filepath string) (*Config, error) { - t, err := toml.LoadFile(filepath) - if err != nil { - return nil, trace.Wrap(err) - } - conf := &Config{} - if err := t.Unmarshal(conf); err != nil { - return nil, trace.Wrap(err) - } - if strings.HasPrefix(conf.Gitlab.Token, "/") { - conf.Gitlab.Token, err = lib.ReadPassword(conf.Gitlab.Token) - if err != nil { - return nil, trace.Wrap(err) - } - } - if err := conf.CheckAndSetDefaults(); err != nil { - return nil, trace.Wrap(err) - } - return conf, nil -} - -func (c *Config) CheckAndSetDefaults() error { - if err := c.Teleport.CheckAndSetDefaults(); err != nil { - return trace.Wrap(err) - } - if c.DB.Path == "" { - c.DB.Path = path.Join(DefaultDir, "/database") - } - if c.Gitlab.Token == "" { - return trace.BadParameter("missing required value gitlab.token") - } - if c.Gitlab.ProjectID == "" { - return trace.BadParameter("missing required value gitlab.project_id") - } - if c.Gitlab.WebhookSecret == "" { - return trace.BadParameter("missing required value gitlab.webhook_secret") - } - if c.HTTP.PublicAddr == "" { - return trace.BadParameter("missing required value http.public_addr") - } - if c.HTTP.ListenAddr == "" { - c.HTTP.ListenAddr = ":8081" - } - if err := c.HTTP.Check(); err != nil { - return trace.Wrap(err) - } - if c.Log.Output == "" { - c.Log.Output = "stderr" - } - if c.Log.Severity == "" { - c.Log.Severity = "info" - } - return nil -} - -// LoadTLSConfig loads client crt/key files and root authorities, and -// generates a tls.Config suitable for use with a GRPC client. -func (c *Config) LoadTLSConfig() (*tls.Config, error) { - var tc tls.Config - clientCert, err := tls.LoadX509KeyPair(c.Teleport.ClientCrt, c.Teleport.ClientKey) - if err != nil { - return nil, trace.Wrap(err) - } - tc.Certificates = append(tc.Certificates, clientCert) - caFile, err := os.Open(c.Teleport.RootCAs) - if err != nil { - return nil, trace.Wrap(err) - } - caCerts, err := ioutil.ReadAll(caFile) - if err != nil { - return nil, trace.Wrap(err) - } - pool := x509.NewCertPool() - if ok := pool.AppendCertsFromPEM(caCerts); !ok { - return nil, trace.BadParameter("invalid CA cert PEM") - } - tc.RootCAs = pool - return &tc, nil -} diff --git a/access/gitlab/database.go b/access/gitlab/database.go deleted file mode 100644 index 85f1fa4b1..000000000 --- a/access/gitlab/database.go +++ /dev/null @@ -1,161 +0,0 @@ -/* -Copyright 2020-2021 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" - "fmt" - "time" - - "github.com/gravitational/trace" - bolt "go.etcd.io/bbolt" -) - -const ( - settingsBucketKey = "settings" - issuesBucketKey = "issues" - hookIDKey = "project-hook-id" -) - -type DB struct{ *bolt.DB } -type SettingsBucket struct{ *bolt.Bucket } -type IssuesBucket struct{ *bolt.Bucket } - -var ErrNoBucket = errors.New("No bucket created yet") - -func OpenDB(path string) (DB, error) { - db, err := bolt.Open(path, 0600, &bolt.Options{ - Timeout: time.Second, - }) - if err != nil { - return DB{}, trace.Wrap(err) - } - return DB{db}, nil -} - -func (db DB) projectBucketKey(projectID IntID) []byte { - return []byte(fmt.Sprintf("project:%d", projectID)) -} - -func (db DB) updateProject(projectID IntID, fn func(*bolt.Bucket) error) error { - return db.Update(func(tx *bolt.Tx) error { - projectBucket, err := tx.CreateBucketIfNotExists(db.projectBucketKey(projectID)) - if err != nil { - return trace.Wrap(err) - } - return fn(projectBucket) - }) -} - -func (db DB) viewProject(projectID IntID, fn func(*bolt.Bucket) error) error { - return db.View(func(tx *bolt.Tx) error { - projectBucket := tx.Bucket(db.projectBucketKey(projectID)) - if projectBucket == nil { - return trace.Wrap(ErrNoBucket) - } - return fn(projectBucket) - }) -} - -func (db DB) UpdateSettings(projectID IntID, fn func(SettingsBucket) error) error { - return db.updateProject(projectID, func(bucket *bolt.Bucket) error { - bucket, err := bucket.CreateBucketIfNotExists([]byte(settingsBucketKey)) - if err != nil { - return trace.Wrap(err) - } - return fn(SettingsBucket{bucket}) - }) -} - -func (db DB) ViewSettings(projectID IntID, fn func(SettingsBucket) error) error { - return db.viewProject(projectID, func(bucket *bolt.Bucket) error { - bucket = bucket.Bucket([]byte(settingsBucketKey)) - if bucket == nil { - return trace.Wrap(ErrNoBucket) - } - return fn(SettingsBucket{bucket}) - }) -} - -func (db DB) UpdateIssues(projectID IntID, fn func(IssuesBucket) error) error { - return db.updateProject(projectID, func(bucket *bolt.Bucket) error { - bucket, err := bucket.CreateBucketIfNotExists([]byte(issuesBucketKey)) - if err != nil { - return trace.Wrap(err) - } - return fn(IssuesBucket{bucket}) - }) -} - -func (db DB) ViewIssues(projectID IntID, fn func(IssuesBucket) error) error { - return db.viewProject(projectID, func(bucket *bolt.Bucket) error { - bucket = bucket.Bucket([]byte(issuesBucketKey)) - if bucket == nil { - return trace.Wrap(ErrNoBucket) - } - return fn(IssuesBucket{bucket}) - }) -} - -func (s SettingsBucket) HookID() IntID { - return BytesToIntID(s.Get([]byte(hookIDKey))) -} - -func (s SettingsBucket) SetHookID(id IntID) error { - return s.Put([]byte(hookIDKey), IntIDToBytes(id)) -} - -func (s SettingsBucket) labelKey(key string) []byte { - return []byte(fmt.Sprintf("label/%s:name", key)) -} - -func (s SettingsBucket) GetLabel(key string) string { - return string(s.Get(s.labelKey(key))) -} - -func (s SettingsBucket) SetLabel(key string, label string) error { - return s.Put(s.labelKey(key), []byte(label)) -} - -func (s SettingsBucket) GetLabels(keys ...string) map[string]string { - mapping := make(map[string]string) - for _, key := range keys { - mapping[key] = s.GetLabel(key) - } - return mapping -} - -func (s SettingsBucket) SetLabels(mapping map[string]string) error { - for key, value := range mapping { - if err := s.SetLabel(key, value); err != nil { - return trace.Wrap(err) - } - } - return nil -} - -func (i IssuesBucket) requestIDKey(issueID IntID) []byte { - return []byte(fmt.Sprintf("%s:request-id", issueID)) -} - -func (i IssuesBucket) GetRequestID(issueID IntID) string { - return string(i.Get(i.requestIDKey(issueID))) -} - -func (i IssuesBucket) SetRequestID(issueID IntID, reqID string) error { - return i.Put(i.requestIDKey(issueID), []byte(reqID)) -} diff --git a/access/gitlab/fake_gitlab_test.go b/access/gitlab/fake_gitlab_test.go deleted file mode 100644 index 4a2c1b21f..000000000 --- a/access/gitlab/fake_gitlab_test.go +++ /dev/null @@ -1,508 +0,0 @@ -/* -Copyright 2020-2021 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" - "encoding/json" - "fmt" - "net/http" - "net/http/httptest" - "runtime/debug" - "strconv" - "strings" - "sync" - "sync/atomic" - - "github.com/gravitational/teleport-plugins/lib/stringset" - "github.com/gravitational/trace" - "github.com/julienschmidt/httprouter" - - log "github.com/sirupsen/logrus" -) - -type FakeGitlab struct { - srv *httptest.Server - - objects sync.Map - // Issues - issueIDCounters sync.Map - newIssues chan Issue - issueUpdates chan Issue - // Notes - noteIDCounter uint64 - newNotes chan Note - // Project hooks - projectHookIDCounter uint64 - newProjectHooks chan ProjectHook - projectHookUpdates chan ProjectHook - // Labels - labelIDCounter uint64 - newLabels chan Label -} - -type fakeLabelByName string -type fakeProjectHookKey struct { - ProjectID IntID - HookIID IntID -} -type fakeProjectIssueKey struct { - ProjectID IntID - IssueIID IntID -} - -func NewFakeGitLab(projectID IntID, concurrency int) *FakeGitlab { - router := httprouter.New() - - gitlab := &FakeGitlab{ - newIssues: make(chan Issue, concurrency), - issueUpdates: make(chan Issue, concurrency), - newNotes: make(chan Note, concurrency), - newProjectHooks: make(chan ProjectHook, concurrency), - projectHookUpdates: make(chan ProjectHook, concurrency), - newLabels: make(chan Label, concurrency), - srv: httptest.NewServer(router), - } - - // Projects - - router.GET("/api/v4/projects/:project_id", func(rw http.ResponseWriter, r *http.Request, _ httprouter.Params) { - rw.Header().Add("Content-Type", "application/json") - err := json.NewEncoder(rw).Encode(Project{ID: projectID}) - panicIf(err) - }) - - // Hooks - - router.GET("/api/v4/projects/:project_id/hooks", func(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) { - projectID := parseIntID(ps.ByName("project_id")) - - var hooks []ProjectHook - gitlab.objects.Range(func(key, value interface{}) bool { - _, ok := key.(fakeProjectHookKey) - if !ok { - return true - } - hook := value.(ProjectHook) - if hook.ProjectID != projectID { - return true - } - hooks = append(hooks, hook) - return true - }) - - rw.Header().Add("Content-Type", "application/json") - err := json.NewEncoder(rw).Encode(hooks) - panicIf(err) - }) - router.POST("/api/v4/projects/:project_id/hooks", func(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) { - rw.Header().Add("Content-Type", "application/json") - - projectID := parseIntID(ps.ByName("project_id")) - - var hook ProjectHook - err := json.NewDecoder(r.Body).Decode(&hook) - panicIf(err) - - hook.ProjectID = projectID - hook = gitlab.StoreProjectHook(hook) - gitlab.newProjectHooks <- hook - - rw.WriteHeader(http.StatusCreated) - err = json.NewEncoder(rw).Encode(&hook) - panicIf(err) - }) - router.PUT("/api/v4/projects/:project_id/hooks/:id", func(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) { - rw.Header().Add("Content-Type", "application/json") - - projectID := parseIntID(ps.ByName("project_id")) - id := parseIntID(ps.ByName("id")) - - hook, found := gitlab.GetProjectHook(projectID, id) - if !found { - rw.WriteHeader(http.StatusNotFound) - err := json.NewEncoder(rw).Encode(ErrorResult{Message: "Hook not found"}) - panicIf(err) - return - } - - err := json.NewDecoder(r.Body).Decode(&hook) - panicIf(err) - hook.ID = id - - gitlab.StoreProjectHook(hook) - gitlab.projectHookUpdates <- hook - - rw.Header().Add("Content-Type", "application/json") - err = json.NewEncoder(rw).Encode(hook) - panicIf(err) - }) - - // Labels - - router.GET("/api/v4/projects/:project_id/labels", func(rw http.ResponseWriter, r *http.Request, _ httprouter.Params) { - var labels []Label - gitlab.objects.Range(func(key, value interface{}) bool { - keyStr, ok := key.(string) - if !ok { - return true - } - if !strings.HasPrefix(keyStr, "label-") { - return true - } - label := value.(Label) - labels = append(labels, label) - return true - }) - - rw.Header().Add("Content-Type", "application/json") - err := json.NewEncoder(rw).Encode(labels) - panicIf(err) - }) - router.POST("/api/v4/projects/:project_id/labels", func(rw http.ResponseWriter, r *http.Request, _ httprouter.Params) { - rw.Header().Add("Content-Type", "application/json") - - var label Label - err := json.NewDecoder(r.Body).Decode(&label) - panicIf(err) - - label, ok := gitlab.StoreLabelIfNotExists(label) - if !ok { - rw.WriteHeader(http.StatusBadRequest) - err = json.NewEncoder(rw).Encode(ErrorResult{Message: "Label already exists"}) - panicIf(err) - return - } - gitlab.newLabels <- label - - rw.WriteHeader(http.StatusCreated) - err = json.NewEncoder(rw).Encode(label) - panicIf(err) - }) - - // Issues - - router.POST("/api/v4/projects/:project_id/issues", func(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) { - rw.Header().Add("Content-Type", "application/json") - - projectID := parseIntID(ps.ByName("project_id")) - - var params IssueParams - err := json.NewDecoder(r.Body).Decode(¶ms) - panicIf(err) - issue := gitlab.StoreIssue(Issue{ - ProjectID: projectID, - Title: params.Title, - Description: params.Description, - State: "opened", - Labels: gitlab.GetLabelTitles(strings.Split(params.Labels, ",")...), - }) - gitlab.newIssues <- issue - - rw.WriteHeader(http.StatusCreated) - err = json.NewEncoder(rw).Encode(issue) - panicIf(err) - }) - router.PUT("/api/v4/projects/:project_id/issues/:issue_iid", func(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) { - rw.Header().Add("Content-Type", "application/json") - - projectID := parseIntID(ps.ByName("project_id")) - issueIID := parseIntID(ps.ByName("issue_iid")) - - issue, found := gitlab.GetProjectIssue(projectID, issueIID) - if !found { - rw.WriteHeader(http.StatusNotFound) - err := json.NewEncoder(rw).Encode(ErrorResult{Message: "Issue not found"}) - panicIf(err) - return - } - - var params IssueParams - err := json.NewDecoder(r.Body).Decode(¶ms) - panicIf(err) - - issue.Title = params.Title - - labels := stringset.New(issue.Labels...) - for _, label := range gitlab.GetLabelTitles(strings.Split(params.AddLabels, ",")...) { - labels.Add(label) - } - for _, label := range gitlab.GetLabelTitles(strings.Split(params.RemoveLabels, ",")...) { - labels.Del(label) - } - issue.Labels = labels.ToSlice() - - switch params.StateEvent { - case "close": - issue.State = "closed" - default: - log.Panicf("unknown StateEvent=%q", params.StateEvent) - } - - gitlab.StoreIssue(issue) - gitlab.issueUpdates <- issue - - rw.Header().Add("Content-Type", "application/json") - err = json.NewEncoder(rw).Encode(issue) - panicIf(err) - }) - router.GET("/api/v4/projects/:project_id/issues/:issue_iid", func(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) { - rw.Header().Add("Content-Type", "application/json") - - projectID := parseIntID(ps.ByName("project_id")) - issueIID := parseIntID(ps.ByName("issue_iid")) - - issue, found := gitlab.GetProjectIssue(projectID, issueIID) - if !found { - rw.WriteHeader(http.StatusNotFound) - err := json.NewEncoder(rw).Encode(ErrorResult{Message: "Issue not found"}) - panicIf(err) - return - } - - err := json.NewEncoder(rw).Encode(issue) - panicIf(err) - }) - - // Issue notes - - router.POST("/api/v4/projects/:project_id/issues/:issue_iid/notes", func(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) { - rw.Header().Add("Content-Type", "application/json") - - projectID := parseIntID(ps.ByName("project_id")) - issueIID := parseIntID(ps.ByName("issue_iid")) - - issue, found := gitlab.GetProjectIssue(projectID, issueIID) - if !found { - rw.WriteHeader(http.StatusNotFound) - err := json.NewEncoder(rw).Encode(ErrorResult{Message: "Issue not found"}) - panicIf(err) - return - } - - var params NoteParams - err := json.NewDecoder(r.Body).Decode(¶ms) - panicIf(err) - note := gitlab.StoreNote(Note{ - NoteableType: "Issue", - NoteableID: issue.ID, - Body: params.Body, - Confidential: params.Confidential, - }) - gitlab.newNotes <- note - - rw.WriteHeader(http.StatusCreated) - err = json.NewEncoder(rw).Encode(note) - panicIf(err) - }) - - return gitlab -} - -func (s *FakeGitlab) URL() string { - return s.srv.URL -} - -func (s *FakeGitlab) Close() { - s.srv.Close() - close(s.newIssues) - close(s.issueUpdates) - close(s.newProjectHooks) - close(s.projectHookUpdates) - close(s.newLabels) -} - -func (s *FakeGitlab) GetIssue(id IntID) (Issue, bool) { - if obj, ok := s.objects.Load(fmt.Sprintf("issue-%d", id)); ok { - return obj.(Issue), true - } - return Issue{}, false -} - -func (s *FakeGitlab) GetProjectIssue(projectID, issueIID IntID) (Issue, bool) { - if obj, ok := s.objects.Load(fakeProjectIssueKey{ProjectID: projectID, IssueIID: issueIID}); ok { - return obj.(Issue), true - } - return Issue{}, false -} - -func (s *FakeGitlab) StoreIssue(issue Issue) Issue { - if issue.IID == 0 { - var newID uint64 - val, _ := s.issueIDCounters.LoadOrStore(issue.ProjectID, &newID) - idPtr := val.(*uint64) - issue.IID = IntID(atomic.AddUint64(idPtr, 1)) - issue.ID = issue.ProjectID*1000000 + issue.IID - } - s.objects.Store(fmt.Sprintf("issue-%d", issue.ID), issue) - s.objects.Store(fakeProjectIssueKey{ProjectID: issue.ProjectID, IssueIID: issue.IID}, issue) - return issue -} - -func (s *FakeGitlab) StoreNote(note Note) Note { - if note.ID == 0 { - note.ID = IntID(atomic.AddUint64(&s.noteIDCounter, 1)) - } - s.objects.Store(fmt.Sprintf("note-%d", note.ID), note) - return note -} - -func (s *FakeGitlab) GetLabelByName(name string) (Label, bool) { - if obj, ok := s.objects.Load(fakeLabelByName(name)); ok { - return obj.(Label), true - } - return Label{}, false -} - -func (s *FakeGitlab) GetLabels(names ...string) (labels []Label) { - for _, name := range names { - name = strings.TrimSpace(name) - if label, exists := s.GetLabelByName(name); exists { - labels = append(labels, label) - } - } - return -} - -func (s *FakeGitlab) GetLabelTitles(names ...string) (titles []string) { - for _, label := range s.GetLabels(names...) { - titles = append(titles, label.Title) - } - return -} - -func (s *FakeGitlab) StoreLabel(label Label) Label { - if label.Name != "" { - label.Title = label.Name - } else { - label.Name = label.Title - } - - if label.ID == 0 { - label.ID = IntID(atomic.AddUint64(&s.labelIDCounter, 1)) - } - s.objects.Store(fmt.Sprintf("label-%d", label.ID), label) - s.objects.Store(fakeLabelByName(label.Name), label) - return label -} - -func (s *FakeGitlab) StoreLabelIfNotExists(label Label) (Label, bool) { - var name string - if label.Name != "" { - name = label.Name - } else { - name = label.Title - } - if existingLabel, exists := s.GetLabelByName(name); exists { - return existingLabel, false - } - - return s.StoreLabel(label), true -} - -func (s *FakeGitlab) GetProjectHook(projectID, hookIID IntID) (ProjectHook, bool) { - if obj, ok := s.objects.Load(fakeProjectHookKey{ProjectID: projectID, HookIID: hookIID}); ok { - return obj.(ProjectHook), true - } - return ProjectHook{}, false -} - -func (s *FakeGitlab) StoreProjectHook(hook ProjectHook) ProjectHook { - if hook.ID == 0 { - hook.ID = IntID(atomic.AddUint64(&s.projectHookIDCounter, 1)) - } - s.objects.Store(fakeProjectHookKey{ProjectID: hook.ProjectID, HookIID: hook.ID}, hook) - return hook -} - -func (s *FakeGitlab) CheckNewProjectHook(ctx context.Context) (ProjectHook, error) { - select { - case hook := <-s.newProjectHooks: - return hook, nil - case <-ctx.Done(): - return ProjectHook{}, trace.Wrap(ctx.Err()) - } -} - -func (s *FakeGitlab) CheckProjectHookUpdate(ctx context.Context) (ProjectHook, error) { - select { - case hook := <-s.projectHookUpdates: - return hook, nil - case <-ctx.Done(): - return ProjectHook{}, trace.Wrap(ctx.Err()) - } -} - -func (s *FakeGitlab) CheckNoNewProjectHooks() bool { - select { - case <-s.newProjectHooks: - return false - default: - return true - } -} - -func (s *FakeGitlab) GetAllNewLabels() map[string]Label { - newLabels := make(map[string]Label) - for { - select { - case label := <-s.newLabels: - newLabels[LabelName(label.Name).Reduced()] = label - default: - return newLabels - } - } -} - -func (s *FakeGitlab) CheckNewIssue(ctx context.Context) (Issue, error) { - select { - case issue := <-s.newIssues: - return issue, nil - case <-ctx.Done(): - return Issue{}, trace.Wrap(ctx.Err()) - } -} - -func (s *FakeGitlab) CheckIssueUpdate(ctx context.Context) (Issue, error) { - select { - case issue := <-s.issueUpdates: - return issue, nil - case <-ctx.Done(): - return Issue{}, trace.Wrap(ctx.Err()) - } -} - -func (s *FakeGitlab) CheckNewNote(ctx context.Context) (Note, error) { - select { - case note := <-s.newNotes: - return note, nil - case <-ctx.Done(): - return Note{}, trace.Wrap(ctx.Err()) - } -} - -func parseIntID(str string) IntID { - val, err := strconv.ParseUint(str, 10, 64) - panicIf(err) - return IntID(val) -} - -func panicIf(err error) { - if err != nil { - log.Panicf("%v at %v", err, string(debug.Stack())) - } -} diff --git a/access/gitlab/gitlab_test.go b/access/gitlab/gitlab_test.go deleted file mode 100644 index c34336730..000000000 --- a/access/gitlab/gitlab_test.go +++ /dev/null @@ -1,961 +0,0 @@ -/* -Copyright 2020-2021 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 ( - "bytes" - "context" - "encoding/json" - "fmt" - "net/http" - "os/user" - "reflect" - "runtime" - "sync" - "sync/atomic" - "testing" - "time" - - "github.com/google/uuid" - - "github.com/gravitational/teleport-plugins/lib" - "github.com/gravitational/teleport-plugins/lib/logger" - . "github.com/gravitational/teleport-plugins/lib/testing" - "github.com/gravitational/teleport-plugins/lib/testing/integration" - "github.com/gravitational/teleport/api/client/proto" - "github.com/gravitational/teleport/api/types" - "github.com/gravitational/trace" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/stretchr/testify/suite" -) - -const ( - WebhookSecret = "0000" - projectID = IntID(1111) -) - -type GitlabSuite struct { - Suite - appConfig Config - userNames struct { - ruler string - requestor string - reviewer1 string - reviewer2 string - plugin string - } - approverEmail string - raceNumber int - dbPath string - fakeGitlab *FakeGitlab - - clients map[string]*integration.Client - teleportFeatures *proto.Features - teleportConfig lib.TeleportConfig -} - -func TestGitlab(t *testing.T) { suite.Run(t, &GitlabSuite{}) } - -func (s *GitlabSuite) SetupSuite() { - var err error - t := s.T() - - logger.Init() - logger.Setup(logger.Config{Severity: "debug"}) - s.raceNumber = runtime.GOMAXPROCS(0) - me, err := user.Current() - require.NoError(t, err) - - s.approverEmail = me.Username + "-approver@example.com" - - // We set such a big timeout because integration.NewFromEnv could start - // downloading a Teleport *-bin.tar.gz file which can take a long time. - ctx := s.SetContextTimeout(2 * time.Minute) - - teleport, err := integration.NewFromEnv(ctx) - require.NoError(t, err) - t.Cleanup(teleport.Close) - - auth, err := teleport.NewAuthService() - require.NoError(t, err) - s.StartApp(auth) - - s.clients = make(map[string]*integration.Client) - - // Set up the user who has an access to all kinds of resources. - - s.userNames.ruler = me.Username + "-ruler@example.com" - client, err := teleport.MakeAdmin(ctx, auth, s.userNames.ruler) - require.NoError(t, err) - s.clients[s.userNames.ruler] = client - - // Get the server features. - - pong, err := client.Ping(ctx) - require.NoError(t, err) - teleportFeatures := pong.GetServerFeatures() - - var bootstrap integration.Bootstrap - - // Set up user who can request the access to role "editor". - - conditions := types.RoleConditions{Request: &types.AccessRequestConditions{Roles: []string{"editor"}}} - if teleportFeatures.AdvancedAccessWorkflows { - conditions.Request.Thresholds = []types.AccessReviewThreshold{types.AccessReviewThreshold{Approve: 2, Deny: 2}} - } - role, err := bootstrap.AddRole("foo", types.RoleSpecV5{Allow: conditions}) - require.NoError(t, err) - - user, err := bootstrap.AddUserWithRoles(me.Username+"@example.com", role.GetName()) - require.NoError(t, err) - s.userNames.requestor = user.GetName() - - if teleportFeatures.AdvancedAccessWorkflows { - // Set up TWO users who can review access requests to role "editor". - - role, err = bootstrap.AddRole("foo-reviewer", types.RoleSpecV5{ - Allow: types.RoleConditions{ - ReviewRequests: &types.AccessReviewConditions{Roles: []string{"editor"}}, - }, - }) - require.NoError(t, err) - - user, err = bootstrap.AddUserWithRoles(me.Username+"-reviewer1@example.com", role.GetName()) - require.NoError(t, err) - s.userNames.reviewer1 = user.GetName() - - user, err = bootstrap.AddUserWithRoles(me.Username+"-reviewer2@example.com", role.GetName()) - require.NoError(t, err) - s.userNames.reviewer2 = user.GetName() - } - - // Set up plugin user. - - role, err = bootstrap.AddRole("access-gitlab", types.RoleSpecV5{ - Allow: types.RoleConditions{ - Rules: []types.Rule{ - types.NewRule("access_request", []string{"list", "read", "update"}), - }, - }, - }) - require.NoError(t, err) - - user, err = bootstrap.AddUserWithRoles("access-gitlab", role.GetName()) - require.NoError(t, err) - s.userNames.plugin = user.GetName() - - // Bake all the resources. - - err = teleport.Bootstrap(ctx, auth, bootstrap.Resources()) - require.NoError(t, err) - - // Initialize the clients. - - client, err = teleport.NewClient(ctx, auth, s.userNames.requestor) - require.NoError(t, err) - s.clients[s.userNames.requestor] = client - - if teleportFeatures.AdvancedAccessWorkflows { - client, err = teleport.NewClient(ctx, auth, s.userNames.reviewer1) - require.NoError(t, err) - s.clients[s.userNames.reviewer1] = client - - client, err = teleport.NewClient(ctx, auth, s.userNames.reviewer2) - require.NoError(t, err) - s.clients[s.userNames.reviewer2] = client - } - - identityPath, err := teleport.Sign(ctx, auth, s.userNames.plugin) - require.NoError(t, err) - - s.teleportConfig.Addr = auth.AuthAddr().String() - s.teleportConfig.Identity = identityPath - s.teleportFeatures = teleportFeatures -} - -func (s *GitlabSuite) SetupTest() { - t := s.T() - - logger.Setup(logger.Config{Severity: "debug"}) - - s.fakeGitlab = NewFakeGitLab(projectID, s.raceNumber) - t.Cleanup(s.fakeGitlab.Close) - - dbFile := s.NewTmpFile("db.*") - s.dbPath = dbFile.Name() - dbFile.Close() - - var conf Config - conf.Teleport = s.teleportConfig - conf.Gitlab.URL = s.fakeGitlab.URL() - conf.Gitlab.WebhookSecret = WebhookSecret - conf.Gitlab.ProjectID = fmt.Sprintf("%d", projectID) - conf.DB.Path = s.dbPath - conf.HTTP.ListenAddr = ":0" - conf.HTTP.Insecure = true - - s.appConfig = conf - s.SetContextTimeout(15 * time.Second) -} - -func (s *GitlabSuite) startApp() *App { - t := s.T() - t.Helper() - - app, err := NewApp(s.appConfig) - require.NoError(t, err) - - s.StartApp(app) - - return app -} - -func (s *GitlabSuite) ruler() *integration.Client { - return s.clients[s.userNames.ruler] -} - -func (s *GitlabSuite) requestor() *integration.Client { - return s.clients[s.userNames.requestor] -} - -func (s *GitlabSuite) reviewer1() *integration.Client { - return s.clients[s.userNames.reviewer1] -} - -func (s *GitlabSuite) reviewer2() *integration.Client { - return s.clients[s.userNames.reviewer2] -} - -func (s *GitlabSuite) newAccessRequest() types.AccessRequest { - t := s.T() - t.Helper() - - req, err := types.NewAccessRequest(uuid.New().String(), s.userNames.requestor, "editor") - require.NoError(t, err) - return req -} - -func (s *GitlabSuite) createAccessRequest() types.AccessRequest { - t := s.T() - t.Helper() - - req := s.newAccessRequest() - err := s.requestor().CreateAccessRequest(s.Context(), req) - require.NoError(t, err) - return req -} - -func (s *GitlabSuite) checkPluginData(reqID string, cond func(PluginData) bool) PluginData { - t := s.T() - t.Helper() - - for { - rawData, err := s.ruler().PollAccessRequestPluginData(s.Context(), "gitlab", reqID) - require.NoError(t, err) - if data := DecodePluginData(rawData); cond(data) { - return data - } - } -} - -func (s *GitlabSuite) assertNewLabels(expected int) map[string]Label { - t := s.T() - t.Helper() - - newLabels := s.fakeGitlab.GetAllNewLabels() - actual := len(newLabels) - assert.GreaterOrEqual(t, expected, actual, "expected %d labels but extra %d labels was stored", expected, actual-expected) - assert.LessOrEqual(t, expected, actual, "expected %d labels but %d labels are missing", expected, expected-actual) - return newLabels -} - -func (s *GitlabSuite) postIssueUpdateHook(ctx context.Context, url string, oldIssue, newIssue Issue) (*http.Response, error) { - var labelsChange *LabelsChange - if !reflect.DeepEqual(oldIssue.Labels, newIssue.Labels) { - labelsChange = &LabelsChange{Previous: s.fakeGitlab.GetLabels(oldIssue.Labels...), Current: s.fakeGitlab.GetLabels(newIssue.Labels...)} - } - payload := IssueEvent{ - Project: Project{ID: projectID}, - User: User{ - Name: "Test User", - Email: s.approverEmail, - }, - ObjectAttributes: IssueObjectAttributes{ - Action: "update", - ID: oldIssue.ID, - IID: oldIssue.IID, - ProjectID: oldIssue.ProjectID, - Description: oldIssue.Description, - }, - Changes: IssueChanges{ - Labels: labelsChange, - }, - } - - var buf bytes.Buffer - err := json.NewEncoder(&buf).Encode(&payload) - if err != nil { - return nil, trace.Wrap(err) - } - - req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, &buf) - if err != nil { - return nil, trace.Wrap(err) - } - req.Header.Add("Content-Type", "application/json") - req.Header.Add("X-Gitlab-Token", WebhookSecret) - req.Header.Add("X-Gitlab-Event", "Issue Hook") - - response, err := http.DefaultClient.Do(req) - return response, trace.Wrap(err) -} - -func (s *GitlabSuite) postIssueUpdateHookAndCheck(url string, oldIssue, newIssue Issue) { - t := s.T() - t.Helper() - - resp, err := s.postIssueUpdateHook(s.Context(), url, oldIssue, newIssue) - require.NoError(t, err) - assert.Equal(t, http.StatusNoContent, resp.StatusCode) - err = resp.Body.Close() - require.NoError(t, err) -} - -func (s *GitlabSuite) openDB(fn func(db DB) error) { - t := s.T() - t.Helper() - - db, err := OpenDB(s.dbPath) - require.NoError(t, err) - defer func() { - if err := db.Close(); err != nil { - panic(err) - } - }() - require.NoError(t, fn(db)) -} - -func (s *GitlabSuite) TestProjectHookSetup() { - t := s.T() - - app := s.startApp() - - hook, err := s.fakeGitlab.CheckNewProjectHook(s.Context()) - require.NoError(t, err, "no new project hooks stored") - assert.Equal(t, app.PublicURL().String()+gitlabWebhookPath, hook.URL) - - err = app.Shutdown(s.Context()) - require.NoError(t, err) - - var dbHookID IntID - s.openDB(func(db DB) error { - return db.ViewSettings(projectID, func(settings SettingsBucket) error { - dbHookID = settings.HookID() - return nil - }) - }) - assert.Equal(t, hook.ID, dbHookID) -} - -func (s *GitlabSuite) TestProjectHookSetupWhenItExists() { - t := s.T() - - s.appConfig.HTTP.PublicAddr = "http://teleport-gitlab.local" - hook := s.fakeGitlab.StoreProjectHook(ProjectHook{ - ProjectID: projectID, - URL: s.appConfig.HTTP.PublicAddr + gitlabWebhookPath, - }) - - app := s.startApp() - err := app.Shutdown(s.Context()) - require.NoError(t, err) - - require.True(t, s.fakeGitlab.CheckNoNewProjectHooks()) - - var dbHookID IntID - s.openDB(func(db DB) error { - return db.ViewSettings(projectID, func(settings SettingsBucket) error { - dbHookID = settings.HookID() - return nil - }) - }) - assert.Equal(t, hook.ID, dbHookID) -} - -func (s *GitlabSuite) TestProjectHookSetupWhenItExistsInDB() { - t := s.T() - - existingHook := s.fakeGitlab.StoreProjectHook(ProjectHook{ - ProjectID: projectID, - URL: "http://fooo", - }) - - s.openDB(func(db DB) error { - return db.UpdateSettings(projectID, func(settings SettingsBucket) error { - return settings.SetHookID(existingHook.ID) - }) - }) - - app := s.startApp() - - hook, err := s.fakeGitlab.CheckProjectHookUpdate(s.Context()) - require.NoError(t, err, "no project hooks updated") - assert.Equal(t, existingHook.ProjectID, hook.ProjectID) - assert.Equal(t, existingHook.ID, hook.ID) - assert.Equal(t, app.PublicURL().String()+gitlabWebhookPath, hook.URL) - - err = app.Shutdown(s.Context()) - require.NoError(t, err) - - var dbHookID IntID - s.openDB(func(db DB) error { - return db.ViewSettings(projectID, func(settings SettingsBucket) error { - dbHookID = settings.HookID() - return nil - }) - }) - assert.Equal(t, existingHook.ID, dbHookID) -} - -func (s *GitlabSuite) TestLabelsSetup() { - t := s.T() - - app := s.startApp() - - newLabels := s.assertNewLabels(4) - assert.Equal(t, "Teleport: Pending", newLabels["pending"].Name) - assert.Equal(t, "Teleport: Approved", newLabels["approved"].Name) - assert.Equal(t, "Teleport: Denied", newLabels["denied"].Name) - assert.Equal(t, "Teleport: Expired", newLabels["expired"].Name) - - err := app.Shutdown(s.Context()) - require.NoError(t, err) - - var dbLabels map[string]string - s.openDB(func(db DB) error { - return db.ViewSettings(projectID, func(settings SettingsBucket) error { - dbLabels = settings.GetLabels("pending", "approved", "denied", "expired") - return nil - }) - }) - assert.Equal(t, newLabels["pending"].Name, dbLabels["pending"]) - assert.Equal(t, newLabels["approved"].Name, dbLabels["approved"]) - assert.Equal(t, newLabels["denied"].Name, dbLabels["denied"]) - assert.Equal(t, newLabels["expired"].Name, dbLabels["expired"]) -} - -func (s *GitlabSuite) TestLabelsSetupWhenSomeExist() { - t := s.T() - - labels := map[string]Label{ - "pending": s.fakeGitlab.StoreLabel(Label{Name: "teleport:pending"}), - "expired": s.fakeGitlab.StoreLabel(Label{Name: "teleport:expired"}), - } - - app := s.startApp() - - newLabels := s.assertNewLabels(2) - assert.Equal(t, "Teleport: Approved", newLabels["approved"].Name) - assert.Equal(t, "Teleport: Denied", newLabels["denied"].Name) - - err := app.Shutdown(s.Context()) - require.NoError(t, err) - - var dbLabels map[string]string - s.openDB(func(db DB) error { - return db.ViewSettings(projectID, func(settings SettingsBucket) error { - dbLabels = settings.GetLabels("pending", "approved", "denied", "expired") - return nil - }) - }) - - assert.Equal(t, labels["pending"].Name, dbLabels["pending"]) - assert.Equal(t, newLabels["approved"].Name, dbLabels["approved"]) - assert.Equal(t, newLabels["denied"].Name, dbLabels["denied"]) - assert.Equal(t, labels["expired"].Name, dbLabels["expired"]) -} - -func (s *GitlabSuite) TestIssueCreation() { - t := s.T() - - app := s.startApp() - - request := s.createAccessRequest() - pluginData := s.checkPluginData(request.GetName(), func(data PluginData) bool { - return data.GitlabData.IssueID != 0 - }) // when issue id is written, we are sure that request is completely served. - - issue, err := s.fakeGitlab.CheckNewIssue(s.Context()) - require.NoError(t, err, "no new issues stored") - assert.Equal(t, projectID, issue.ProjectID) - assert.Len(t, issue.Labels, 1) - assert.Equal(t, "pending", LabelName(issue.Labels[0]).Reduced()) - - assert.Equal(t, issue.ProjectID, pluginData.ProjectID) - assert.Equal(t, issue.IID, pluginData.IssueIID) - assert.Equal(t, issue.ID, pluginData.IssueID) - - err = app.Shutdown(s.Context()) - require.NoError(t, err) - - var reqID string - s.openDB(func(db DB) error { - return db.ViewIssues(projectID, func(issues IssuesBucket) error { - reqID = issues.GetRequestID(issue.IID) - return nil - }) - }) - - assert.Equal(t, request.GetName(), reqID) -} - -func (s *GitlabSuite) TestReviewComments() { - t := s.T() - - if !s.teleportFeatures.AdvancedAccessWorkflows { - t.Skip("Doesn't work in OSS version") - } - - s.startApp() - req := s.createAccessRequest() - - err := s.reviewer1().SubmitAccessRequestReview(s.Context(), req.GetName(), types.AccessReview{ - Author: s.userNames.reviewer1, - ProposedState: types.RequestState_APPROVED, - Created: time.Now(), - Reason: "okay", - }) - require.NoError(t, err) - err = s.reviewer2().SubmitAccessRequestReview(s.Context(), req.GetName(), types.AccessReview{ - Author: s.userNames.reviewer2, - ProposedState: types.RequestState_DENIED, - Created: time.Now(), - Reason: "not okay", - }) - require.NoError(t, err) - - pluginData := s.checkPluginData(req.GetName(), func(data PluginData) bool { - return data.GitlabData.IssueID != 0 && data.ReviewsCount == 2 - }) - issueID := pluginData.IssueID - - note, err := s.fakeGitlab.CheckNewNote(s.Context()) - require.NoError(t, err) - assert.Equal(t, "Issue", note.NoteableType) - assert.Equal(t, issueID, note.NoteableID) - assert.Contains(t, note.Body, "**"+s.userNames.reviewer1+"** reviewed the request", "comment must contain a review author") - assert.Contains(t, note.Body, "Resolution: **APPROVED**", "comment must contain an approval resolution") - assert.Contains(t, note.Body, "Reason: okay", "comment must contain an approval reason") - - note, err = s.fakeGitlab.CheckNewNote(s.Context()) - require.NoError(t, err) - assert.Equal(t, "Issue", note.NoteableType) - assert.Equal(t, issueID, note.NoteableID) - assert.Contains(t, note.Body, "**"+s.userNames.reviewer2+"** reviewed the request", "comment must contain a review author") - assert.Contains(t, note.Body, "Resolution: **DENIED**", "comment must contain an denial resolution") - assert.Contains(t, note.Body, "Reason: not okay", "comment must contain an denial reason") -} - -func (s *GitlabSuite) TestReviewerApproval() { - t := s.T() - - if !s.teleportFeatures.AdvancedAccessWorkflows { - t.Skip("Doesn't work in OSS version") - } - - s.startApp() - req := s.createAccessRequest() - - pluginData := s.checkPluginData(req.GetName(), func(data PluginData) bool { - return data.IssueID != 0 - }) - issueID := pluginData.IssueID - - err := s.reviewer1().SubmitAccessRequestReview(s.Context(), req.GetName(), types.AccessReview{ - Author: s.userNames.reviewer1, - ProposedState: types.RequestState_APPROVED, - Created: time.Now(), - Reason: "okay", - }) - require.NoError(t, err) - - note, err := s.fakeGitlab.CheckNewNote(s.Context()) - require.NoError(t, err) - assert.Equal(t, "Issue", note.NoteableType) - assert.Equal(t, issueID, note.NoteableID) - assert.Contains(t, note.Body, "**"+s.userNames.reviewer1+"** reviewed the request", "comment must contain a review author") - - err = s.reviewer2().SubmitAccessRequestReview(s.Context(), req.GetName(), types.AccessReview{ - Author: s.userNames.reviewer2, - ProposedState: types.RequestState_APPROVED, - Created: time.Now(), - Reason: "finally okay", - }) - require.NoError(t, err) - - note, err = s.fakeGitlab.CheckNewNote(s.Context()) - require.NoError(t, err) - assert.Equal(t, "Issue", note.NoteableType) - assert.Equal(t, issueID, note.NoteableID) - assert.Contains(t, note.Body, "**"+s.userNames.reviewer2+"** reviewed the request", "comment must contain a review author") - - pluginData = s.checkPluginData(req.GetName(), func(data PluginData) bool { - return data.IssueID != 0 && data.ReviewsCount == 2 && data.Resolution.Tag != Unresolved - }) - assert.Equal(t, issueID, pluginData.IssueID) - assert.Equal(t, Resolution{Tag: ResolvedApproved, Reason: "finally okay"}, pluginData.Resolution) - - issue, err := s.fakeGitlab.CheckIssueUpdate(s.Context()) - require.NoError(t, err, "no issues updated") - assert.Equal(t, issueID, issue.ID) - assert.Len(t, issue.Labels, 1) - assert.Equal(t, "approved", LabelName(issue.Labels[0]).Reduced()) - assert.Equal(t, "closed", issue.State) - - note, err = s.fakeGitlab.CheckNewNote(s.Context()) - require.NoError(t, err) - assert.Equal(t, "Issue", note.NoteableType) - assert.Equal(t, issueID, note.NoteableID) - assert.Contains(t, note.Body, "Access request has been approved") - assert.Contains(t, note.Body, "Reason: finally okay") -} - -func (s *GitlabSuite) TestReviewerDenial() { - t := s.T() - - if !s.teleportFeatures.AdvancedAccessWorkflows { - t.Skip("Doesn't work in OSS version") - } - - s.startApp() - req := s.createAccessRequest() - - pluginData := s.checkPluginData(req.GetName(), func(data PluginData) bool { - return data.IssueID != 0 - }) - issueID := pluginData.IssueID - - err := s.reviewer1().SubmitAccessRequestReview(s.Context(), req.GetName(), types.AccessReview{ - Author: s.userNames.reviewer1, - ProposedState: types.RequestState_DENIED, - Created: time.Now(), - Reason: "not okay", - }) - require.NoError(t, err) - - note, err := s.fakeGitlab.CheckNewNote(s.Context()) - require.NoError(t, err) - assert.Equal(t, "Issue", note.NoteableType) - assert.Equal(t, issueID, note.NoteableID) - assert.Contains(t, note.Body, "**"+s.userNames.reviewer1+"** reviewed the request", "comment must contain a review author") - - err = s.reviewer2().SubmitAccessRequestReview(s.Context(), req.GetName(), types.AccessReview{ - Author: s.userNames.reviewer2, - ProposedState: types.RequestState_DENIED, - Created: time.Now(), - Reason: "finally not okay", - }) - require.NoError(t, err) - - note, err = s.fakeGitlab.CheckNewNote(s.Context()) - require.NoError(t, err) - assert.Equal(t, "Issue", note.NoteableType) - assert.Equal(t, issueID, note.NoteableID) - assert.Contains(t, note.Body, "**"+s.userNames.reviewer2+"** reviewed the request", "comment must contain a review author") - - pluginData = s.checkPluginData(req.GetName(), func(data PluginData) bool { - return data.IssueID != 0 && data.ReviewsCount == 2 && data.Resolution.Tag != Unresolved - }) - assert.Equal(t, issueID, pluginData.IssueID) - assert.Equal(t, Resolution{Tag: ResolvedDenied, Reason: "finally not okay"}, pluginData.Resolution) - - issue, err := s.fakeGitlab.CheckIssueUpdate(s.Context()) - require.NoError(t, err, "no issues updated") - assert.Equal(t, issueID, issue.ID) - assert.Len(t, issue.Labels, 1) - assert.Equal(t, "denied", LabelName(issue.Labels[0]).Reduced()) - assert.Equal(t, "closed", issue.State) - - note, err = s.fakeGitlab.CheckNewNote(s.Context()) - require.NoError(t, err) - assert.Equal(t, "Issue", note.NoteableType) - assert.Equal(t, issueID, note.NoteableID) - assert.Contains(t, note.Body, "Access request has been denied") - assert.Contains(t, note.Body, "Reason: finally not okay") -} - -func (s *GitlabSuite) TestWebhookApproval() { - t := s.T() - - app := s.startApp() - - labels := s.assertNewLabels(4) - request := s.createAccessRequest() - pluginData := s.checkPluginData(request.GetName(), func(data PluginData) bool { - return data.GitlabData.IssueID != 0 - }) - issueID := pluginData.IssueID - - issue, err := s.fakeGitlab.CheckNewIssue(s.Context()) - require.NoError(t, err, "no new issues stored") - assert.Equal(t, issueID, issue.ID) - assert.Len(t, issue.Labels, 1) - assert.Equal(t, "pending", LabelName(issue.Labels[0]).Reduced()) - - oldIssue := issue - issue.Labels = []string{labels["approved"].Title} - s.fakeGitlab.StoreIssue(issue) - s.postIssueUpdateHookAndCheck(app.PublicURL().String()+gitlabWebhookPath, oldIssue, issue) - - issue, err = s.fakeGitlab.CheckIssueUpdate(s.Context()) - require.NoError(t, err, "no issues updated") - assert.Equal(t, issueID, issue.ID) - assert.Len(t, issue.Labels, 1) - assert.Equal(t, "approved", LabelName(issue.Labels[0]).Reduced()) - assert.Equal(t, "closed", issue.State) - - request, err = s.ruler().GetAccessRequest(s.Context(), request.GetName()) - require.NoError(t, err) - assert.Equal(t, types.RequestState_APPROVED, request.GetState()) - - events, err := s.ruler().SearchAccessRequestEvents(s.Context(), request.GetName()) - require.NoError(t, err) - assert.Len(t, events, 1) - assert.Equal(t, "APPROVED", events[0].RequestState) - assert.Equal(t, "gitlab:"+s.approverEmail, events[0].Delegator) - - note, err := s.fakeGitlab.CheckNewNote(s.Context()) - require.NoError(t, err) - assert.Equal(t, "Issue", note.NoteableType) - assert.Equal(t, issueID, note.NoteableID) - assert.Contains(t, note.Body, "Access request has been approved") -} - -func (s *GitlabSuite) TestWebhookDenial() { - t := s.T() - - app := s.startApp() - - labels := s.assertNewLabels(4) - request := s.createAccessRequest() - pluginData := s.checkPluginData(request.GetName(), func(data PluginData) bool { - return data.GitlabData.IssueID != 0 - }) - issueID := pluginData.IssueID - - issue, err := s.fakeGitlab.CheckNewIssue(s.Context()) - require.NoError(t, err, "no new issues stored") - assert.Equal(t, issueID, issue.ID) - assert.Len(t, issue.Labels, 1) - assert.Equal(t, "pending", LabelName(issue.Labels[0]).Reduced()) - - oldIssue := issue - issue.Labels = []string{labels["denied"].Title} - s.fakeGitlab.StoreIssue(issue) - s.postIssueUpdateHookAndCheck(app.PublicURL().String()+gitlabWebhookPath, oldIssue, issue) - - issue, err = s.fakeGitlab.CheckIssueUpdate(s.Context()) - require.NoError(t, err, "no issues updated") - assert.Equal(t, issueID, issue.ID) - assert.Len(t, issue.Labels, 1) - assert.Equal(t, "denied", LabelName(issue.Labels[0]).Reduced()) - assert.Equal(t, "closed", issue.State) - - request, err = s.ruler().GetAccessRequest(s.Context(), request.GetName()) - require.NoError(t, err) - assert.Equal(t, types.RequestState_DENIED, request.GetState()) - - events, err := s.ruler().SearchAccessRequestEvents(s.Context(), request.GetName()) - require.NoError(t, err) - assert.Len(t, events, 1) - assert.Equal(t, "DENIED", events[0].RequestState) - assert.Equal(t, "gitlab:"+s.approverEmail, events[0].Delegator) - - note, err := s.fakeGitlab.CheckNewNote(s.Context()) - require.NoError(t, err) - assert.Equal(t, "Issue", note.NoteableType) - assert.Equal(t, issueID, note.NoteableID) - assert.Contains(t, note.Body, "Access request has been denied") -} - -func (s *GitlabSuite) TestExpiration() { - t := s.T() - - s.startApp() - - request := s.createAccessRequest() - pluginData := s.checkPluginData(request.GetName(), func(data PluginData) bool { - return data.IssueID != 0 - }) - issueID := pluginData.IssueID - - issue, err := s.fakeGitlab.CheckNewIssue(s.Context()) - require.NoError(t, err, "no new issues stored") - assert.Equal(t, issueID, issue.ID) - assert.Len(t, issue.Labels, 1) - assert.Equal(t, "pending", LabelName(issue.Labels[0]).Reduced()) - - err = s.ruler().DeleteAccessRequest(s.Context(), request.GetName()) // simulate expiration - require.NoError(t, err) - - issue, err = s.fakeGitlab.CheckIssueUpdate(s.Context()) - require.NoError(t, err, "no issues updated") - assert.Equal(t, issueID, issue.ID) - assert.Len(t, issue.Labels, 1) - assert.Equal(t, "expired", LabelName(issue.Labels[0]).Reduced()) - assert.Equal(t, "closed", issue.State) - - note, err := s.fakeGitlab.CheckNewNote(s.Context()) - require.NoError(t, err) - assert.Equal(t, "Issue", note.NoteableType) - assert.Equal(t, issueID, note.NoteableID) - assert.Contains(t, note.Body, "Access request has been expired") -} - -func (s *GitlabSuite) TestRace() { - t := s.T() - - logger.Setup(logger.Config{Severity: "info"}) // Turn off noisy debug logging - - s.SetContextTimeout(30 * time.Second) - app := s.startApp() - - labels := s.assertNewLabels(4) - - var ( - raceErr error - raceErrOnce sync.Once - requests sync.Map - ) - setRaceErr := func(err error) error { - raceErrOnce.Do(func() { - raceErr = err - }) - return err - } - - watcher, err := s.ruler().NewWatcher(s.Context(), types.Watch{ - Kinds: []types.WatchKind{ - { - Kind: types.KindAccessRequest, - }, - }, - }) - require.NoError(t, err) - defer watcher.Close() - assert.Equal(t, types.OpInit, (<-watcher.Events()).Type) - - process := lib.NewProcess(s.Context()) - for i := 0; i < s.raceNumber; i++ { - process.SpawnCritical(func(ctx context.Context) error { - req, err := types.NewAccessRequest(uuid.New().String(), s.userNames.requestor, "editor") - if err != nil { - return setRaceErr(trace.Wrap(err)) - } - if err = s.requestor().CreateAccessRequest(ctx, req); err != nil { - return setRaceErr(trace.Wrap(err)) - } - return nil - }) - process.SpawnCritical(func(ctx context.Context) error { - issue, err := s.fakeGitlab.CheckNewIssue(ctx) - if err := trace.Wrap(err); err != nil { - return setRaceErr(err) - } - if obtained, expected := len(issue.Labels), 1; obtained != expected { - return setRaceErr(trace.Errorf("wrong labels size. expected %v, obtained %v", expected, obtained)) - } - if obtained, expected := LabelName(issue.Labels[0]).Reduced(), "pending"; obtained != expected { - return setRaceErr(trace.Errorf("wrong label. expected %s, obtained %s", expected, obtained)) - } - - oldIssue := issue - issue.Labels = []string{labels["approved"].Name} - s.fakeGitlab.StoreIssue(issue) - - ctx, cancel := context.WithTimeout(ctx, 2*time.Second) - defer cancel() - var lastErr error - for { - logger.Get(ctx).Infof("Trying to approve issue %v", issue.ID) - resp, err := s.postIssueUpdateHook(ctx, app.PublicURL().String()+gitlabWebhookPath, oldIssue, issue) - if err != nil { - if lib.IsDeadline(err) { - return setRaceErr(lastErr) - } - return setRaceErr(trace.Wrap(err)) - } - if err := resp.Body.Close(); err != nil { - return setRaceErr(trace.Wrap(err)) - } - if status := resp.StatusCode; status != http.StatusNoContent { - lastErr = trace.Errorf("got %v http code from webhook server", status) - } else { - return nil - } - } - }) - process.SpawnCritical(func(ctx context.Context) error { - issue, err := s.fakeGitlab.CheckIssueUpdate(ctx) - if err := trace.Wrap(err); err != nil { - return setRaceErr(err) - } - if obtained, expected := issue.State, "closed"; obtained != expected { - return setRaceErr(trace.Errorf("wrong issue state. expected %s, obtained %s", expected, obtained)) - } - return nil - }) - } - for i := 0; i < 2*s.raceNumber; i++ { - process.SpawnCritical(func(ctx context.Context) error { - var event types.Event - select { - case event = <-watcher.Events(): - case <-ctx.Done(): - return setRaceErr(trace.Wrap(ctx.Err())) - } - if obtained, expected := event.Type, types.OpPut; obtained != expected { - return setRaceErr(trace.Errorf("wrong event type. expected %v, obtained %v", expected, obtained)) - } - req := event.Resource.(types.AccessRequest) - var newCounter int64 - val, _ := requests.LoadOrStore(req.GetName(), &newCounter) - switch state := req.GetState(); state { - case types.RequestState_PENDING: - atomic.AddInt64(val.(*int64), 1) - case types.RequestState_APPROVED: - atomic.AddInt64(val.(*int64), -1) - default: - return setRaceErr(trace.Errorf("wrong request state %v", state)) - } - return nil - }) - } - process.Terminate() - <-process.Done() - require.NoError(t, raceErr) - - var count int - requests.Range(func(key, val interface{}) bool { - count++ - assert.Equal(t, int64(0), *val.(*int64)) - return true - }) - assert.Equal(t, s.raceNumber, count) -} diff --git a/access/gitlab/install b/access/gitlab/install deleted file mode 100755 index fd7626b62..000000000 --- a/access/gitlab/install +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/sh - -# -# the directory where Teleport binaries will be located -# -BINDIR=/usr/local/bin - -# the directory where Teleport plugins store their certificates -# and other data files -# -DATADIR=/var/lib/teleport/plugins/gitlab - -[ ! $(id -u) != "0" ] || { echo "ERROR: You must be root"; exit 1; } -cd $(dirname $0) -mkdir -p $BINDIR $DATADIR -cp -f teleport-gitlab $BINDIR/ || exit 1 - -echo "Teleport GitLab Plugin binaries have been copied to $BINDIR" -echo "You can run teleport-gitlab configure > /etc/teleport-gitlab.toml to bootstrap your config file." diff --git a/access/gitlab/main.go b/access/gitlab/main.go deleted file mode 100644 index 990538913..000000000 --- a/access/gitlab/main.go +++ /dev/null @@ -1,102 +0,0 @@ -/* -Copyright 2020-2021 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" - "os" - "time" - - "github.com/gravitational/teleport-plugins/lib" - "github.com/gravitational/teleport-plugins/lib/logger" - - "github.com/gravitational/kingpin" - "github.com/gravitational/trace" -) - -const ( - DefaultDir = "/var/lib/teleport/plugins/gitlab" -) - -func main() { - logger.Init() - app := kingpin.New("teleport-gitlab", "Teleport plugin for access requests approval via GitLab.") - - app.Command("configure", "Prints an example .TOML configuration file.") - app.Command("version", "Prints teleport-gitlab version and exits.") - - startCmd := app.Command("start", "Starts a Teleport GitLab Plugin.") - path := startCmd.Flag("config", "TOML config file path"). - Short('c'). - Default("/etc/teleport-gitlab.toml"). - String() - debug := startCmd.Flag("debug", "Enable verbose logging to stderr"). - Short('d'). - Bool() - insecure := startCmd.Flag("insecure-no-tls", "Disable TLS for the callback server"). - Default("false"). - Bool() - - selectedCmd, err := app.Parse(os.Args[1:]) - if err != nil { - lib.Bail(err) - } - - switch selectedCmd { - case "configure": - fmt.Print(exampleConfig) - case "version": - lib.PrintVersion(app.Name, Version, Gitref) - case "start": - if err := run(*path, *insecure, *debug); err != nil { - lib.Bail(err) - } else { - logger.Standard().Info("Successfully shut down") - } - } -} - -func run(configPath string, insecure bool, debug bool) error { - conf, err := LoadConfig(configPath) - if err != nil { - return trace.Wrap(err) - } - - logConfig := conf.Log - if debug { - logConfig.Severity = "debug" - } - if err = logger.Setup(logConfig); err != nil { - return err - } - if debug { - logger.Standard().Debugf("DEBUG logging enabled") - } - - conf.HTTP.Insecure = insecure - app, err := NewApp(*conf) - if err != nil { - return trace.Wrap(err) - } - - go lib.ServeSignals(app, 15*time.Second) - - return trace.Wrap( - app.Run(context.Background()), - ) -} diff --git a/access/gitlab/plugindata.go b/access/gitlab/plugindata.go deleted file mode 100644 index 910e1ecbe..000000000 --- a/access/gitlab/plugindata.go +++ /dev/null @@ -1,117 +0,0 @@ -/* -Copyright 2021 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" - "time" -) - -// PluginData is a data associated with access request that we store in Teleport using UpdatePluginData API. -type PluginData struct { - RequestData - GitlabData -} - -type Resolution struct { - Tag ResolutionTag - Reason string -} -type ResolutionTag string - -const Unresolved = ResolutionTag("") -const ResolvedApproved = ResolutionTag("approved") -const ResolvedDenied = ResolutionTag("denied") -const ResolvedExpired = ResolutionTag("expired") - -type RequestData struct { - User string - Roles []string - Created time.Time - RequestReason string - ReviewsCount int - Resolution Resolution -} - -type GitlabData struct { - IssueID IntID - IssueIID IntID - ProjectID IntID -} - -// DecodePluginData deserializes a string map to PluginData struct. -func DecodePluginData(dataMap map[string]string) (data PluginData) { - data.User = dataMap["user"] - if str := dataMap["roles"]; str != "" { - data.Roles = strings.Split(str, ",") - } - if str := dataMap["created"]; str != "" { - var created int64 - fmt.Sscanf(str, "%d", &created) - data.Created = time.Unix(created, 0) - } - data.RequestReason = dataMap["request_reason"] - if str := dataMap["reviews_count"]; str != "" { - fmt.Sscanf(str, "%d", &data.ReviewsCount) - } - data.Resolution.Tag = ResolutionTag(dataMap["resolution"]) - data.Resolution.Reason = dataMap["resolve_reason"] - if str := dataMap["project_id"]; str != "" { - fmt.Sscanf(str, "%d", &data.ProjectID) - } - if str := dataMap["issue_iid"]; str != "" { - fmt.Sscanf(str, "%d", &data.IssueIID) - } - if str := dataMap["issue_id"]; str != "" { - fmt.Sscanf(str, "%d", &data.IssueID) - } - return -} - -// EncodePluginData serializes a PluginData struct into a string map. -func EncodePluginData(data PluginData) map[string]string { - result := make(map[string]string) - - result["project_id"] = encodeUInt64(uint64(data.ProjectID)) - result["issue_iid"] = encodeUInt64(uint64(data.IssueIID)) - result["issue_id"] = encodeUInt64(uint64(data.IssueID)) - - result["user"] = data.User - result["roles"] = strings.Join(data.Roles, ",") - - var createdStr string - if !data.Created.IsZero() { - createdStr = fmt.Sprintf("%d", data.Created.Unix()) - } - result["created"] = createdStr - - result["request_reason"] = data.RequestReason - result["reviews_count"] = encodeUInt64(uint64(data.ReviewsCount)) - - result["resolution"] = string(data.Resolution.Tag) - result["resolve_reason"] = data.Resolution.Reason - - return result -} - -func encodeUInt64(val uint64) string { - if val == 0 { - return "" - } - return fmt.Sprintf("%d", val) -} diff --git a/access/gitlab/plugindata_test.go b/access/gitlab/plugindata_test.go deleted file mode 100644 index 2c8818155..000000000 --- a/access/gitlab/plugindata_test.go +++ /dev/null @@ -1,84 +0,0 @@ -/* -Copyright 2021 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 ( - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -var samplePluginData = PluginData{ - RequestData: RequestData{ - User: "user-foo", - Roles: []string{"role-foo", "role-bar"}, - Created: time.Date(2021, 6, 1, 13, 27, 17, 0, time.UTC).Local(), - RequestReason: "foo reason", - ReviewsCount: 3, - Resolution: Resolution{Tag: ResolvedApproved, Reason: "foo ok"}, - }, - GitlabData: GitlabData{ - ProjectID: IntID(333), - IssueID: IntID(1111), - IssueIID: IntID(11), - }, -} - -func TestEncodePluginData(t *testing.T) { - dataMap := EncodePluginData(samplePluginData) - assert.Len(t, dataMap, 10) - assert.Equal(t, "user-foo", dataMap["user"]) - assert.Equal(t, "role-foo,role-bar", dataMap["roles"]) - assert.Equal(t, "1622554037", dataMap["created"]) - assert.Equal(t, "foo reason", dataMap["request_reason"]) - assert.Equal(t, "3", dataMap["reviews_count"]) - assert.Equal(t, "approved", dataMap["resolution"]) - assert.Equal(t, "foo ok", dataMap["resolve_reason"]) - assert.Equal(t, "333", dataMap["project_id"]) - assert.Equal(t, "1111", dataMap["issue_id"]) - assert.Equal(t, "11", dataMap["issue_iid"]) -} - -func TestDecodePluginData(t *testing.T) { - pluginData := DecodePluginData(map[string]string{ - "user": "user-foo", - "roles": "role-foo,role-bar", - "created": "1622554037", - "request_reason": "foo reason", - "reviews_count": "3", - "resolution": "approved", - "resolve_reason": "foo ok", - "project_id": "333", - "issue_id": "1111", - "issue_iid": "11", - }) - assert.Equal(t, samplePluginData, pluginData) -} - -func TestEncodeEmptyPluginData(t *testing.T) { - dataMap := EncodePluginData(PluginData{}) - assert.Len(t, dataMap, 10) - for key, value := range dataMap { - assert.Emptyf(t, value, "value at key %q must be empty", key) - } -} - -func TestDecodeEmptyPluginData(t *testing.T) { - assert.Empty(t, DecodePluginData(nil)) - assert.Empty(t, DecodePluginData(make(map[string]string))) -} diff --git a/access/gitlab/types.go b/access/gitlab/types.go deleted file mode 100644 index a54095fc8..000000000 --- a/access/gitlab/types.go +++ /dev/null @@ -1,237 +0,0 @@ -/* -Copyright 2020-2021 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" - "encoding/binary" - "regexp" - "sort" - "strconv" - "strings" - "unicode" -) - -const ( - NoAction ActionID = iota - ApproveAction - DenyAction -) - -type ActionID int - -type IntID uint64 - -type LabelName string - -type LabelParams struct { - Name string `json:"name,omitempty"` - Color string `json:"color,omitempty"` -} - -type Label struct { - ID IntID `json:"id"` - - // Title and Name really are the same things but both used in different contexts :( - Title string `json:"title"` - Name string `json:"name"` -} - -type LabelsChange struct { - Previous []Label `json:"previous"` - Current []Label `json:"current"` -} - -type SortableLabels []Label - -type Project struct { - ID IntID `json:"id"` -} - -type HookParams struct { - URL string `json:"url,omitempty"` - EnableIssueEvents bool `json:"issues_events,omitempty"` - Token string `json:"token,omitempty"` -} - -type ProjectHook struct { - ID IntID `json:"id"` - ProjectID IntID `json:"project_id"` - URL string `json:"url,omitempty"` -} - -type IssueParams struct { - Title string `json:"title,omitempty"` - Description string `json:"description,omitempty"` - Labels string `json:"labels,omitempty"` - StateEvent string `json:"state_event,omitempty"` - RemoveLabels string `json:"remove_labels,omitempty"` - AddLabels string `json:"add_labels,omitempty"` -} - -type Issue struct { - ID IntID `json:"id,omitempty"` - IID IntID `json:"iid,omitempty"` - ProjectID IntID `json:"project_id,omitempty"` - Title string `json:"title,omitempty"` - Description string `json:"description,omitempty"` - State string `json:"state,omitempty"` - Labels []string `json:"labels,omitempty"` -} - -type IssueObjectAttributes struct { - Action string `json:"action,omitempty"` - ID IntID `json:"id,omitempty"` - IID IntID `json:"iid,omitempty"` - ProjectID IntID `json:"project_id,omitempty"` - Description string `json:"description,omitempty"` -} - -type IssueChanges struct { - Labels *LabelsChange `json:"labels,omitempty"` -} - -type User struct { - Name string `json:"name,omitempty"` - Username string `json:"username,omitempty"` - Email string `json:"email,omitempty"` -} - -type Note struct { - ID IntID `json:"id"` - NoteableType string `json:"noteable_type"` //nolint:misspell - NoteableID IntID `json:"noteable_id"` //nolint:misspell - Body string `json:"body"` - Confidential bool `json:"confidential"` -} - -type NoteParams struct { - Body string `json:"body"` - Confidential bool `json:"confidential,omitempty"` -} - -type IssueEvent struct { - User User `json:"user"` - Project Project `json:"project"` - ObjectAttributes IssueObjectAttributes `json:"object_attributes"` - Changes IssueChanges `json:"changes"` -} - -type Webhook struct { - Event interface{} -} - -type WebhookFunc func(ctx context.Context, hook Webhook) error - -type ErrorResult struct { - Error string `json:"error,omitempty"` - Message interface{} `json:"message,omitempty"` -} - -var issueDescriptionRegex = regexp.MustCompile(`(?i)request\s+id\s+is.+([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})`) - -func (id IntID) String() string { - return strconv.FormatUint(uint64(id), 10) -} - -func IntIDToBytes(id IntID) []byte { - data := make([]byte, 8) - binary.BigEndian.PutUint64(data, uint64(id)) - return data -} - -func BytesToIntID(data []byte) IntID { - if data != nil { - return IntID(binary.BigEndian.Uint64(data)) - } - return 0 -} - -// ToAction maps label name like "Teleport: Approved", "teleport : denied", etc to a specific action. -func (name LabelName) ToAction() ActionID { - switch name.Reduced() { - case "approved": - return ApproveAction - case "denied": - return DenyAction - default: - return NoAction - } -} - -// Reduced maps label name like "Teleport: Approved", "teleport : denied" to "approved", "denied", etc. -func (name LabelName) Reduced() string { - substrs := strings.SplitN(strings.ToLower(string(name)), ":", 2) - if len(substrs) != 2 || strings.TrimFunc(substrs[0], unicode.IsSpace) != "teleport" { - return "" - } - return strings.TrimFunc(substrs[1], unicode.IsSpace) -} - -// NewSortableLabels is a Label slice wrapper that implements sort.Interface sorting by label ID. -func NewSortableLabels(slice []Label) SortableLabels { - if slice == nil { - return SortableLabels(nil) - } - labels := make([]Label, len(slice)) - copy(labels, slice) - return SortableLabels(labels) -} - -func (labels SortableLabels) Len() int { return len(labels) } -func (labels SortableLabels) Swap(i, j int) { labels[i], labels[j] = labels[j], labels[i] } -func (labels SortableLabels) Less(i, j int) bool { return labels[i].ID < labels[j].ID } - -// Diff subtracts previous labels from current. -func (labels *LabelsChange) Diff() []Label { - currentLabels := NewSortableLabels(labels.Current) - previousLabels := NewSortableLabels(labels.Previous) - sort.Sort(currentLabels) - sort.Sort(previousLabels) - - var diff []Label - for i, j := 0, 0; i < currentLabels.Len(); { - if j < previousLabels.Len() { - currentID, previousID := currentLabels[i].ID, previousLabels[j].ID - switch { - case currentID == previousID: - i++ - j++ - case currentID < previousID: - diff = append(diff, currentLabels[i]) - i++ - case currentID > previousID: - j++ - } - } else { - diff = append(diff, currentLabels[i]) - i++ - - } - } - return diff -} - -// ParseDescriptionRequestID is a fallback for searching request id in the issue description -// if it's missing in the database. -func (issue IssueObjectAttributes) ParseDescriptionRequestID() string { - submatches := issueDescriptionRegex.FindStringSubmatch(issue.Description) - if len(submatches) > 1 { - return submatches[1] - } - return "" -} diff --git a/access/gitlab/version.go b/access/gitlab/version.go deleted file mode 100644 index 73617d6e9..000000000 --- a/access/gitlab/version.go +++ /dev/null @@ -1,25 +0,0 @@ -/* -Copyright 2015-2021 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 - -var ( - // Version package version, specified in Makefile using ldflags - Version = `Not specified, use --ldflags "-X main.Version "1.0.0""` - - // Gitref variable is specified in Makefile using ldflags - Gitref string -) diff --git a/access/gitlab/webhook_server.go b/access/gitlab/webhook_server.go deleted file mode 100644 index ccbff08cb..000000000 --- a/access/gitlab/webhook_server.go +++ /dev/null @@ -1,145 +0,0 @@ -/* -Copyright 2020-2021 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" - "crypto/subtle" - "encoding/json" - "fmt" - "io" - "io/ioutil" - "net/http" - "net/url" - "sync/atomic" - "time" - - "github.com/julienschmidt/httprouter" - - "github.com/gravitational/teleport-plugins/lib" - "github.com/gravitational/teleport-plugins/lib/logger" - "github.com/gravitational/trace" -) - -const ( - gitlabWebhookPath = "/webhook" - - // as per https://docs.gitlab.com/ee/user/gitlab_com/index.html#webhooks, - // the webhook payload size is limited to 25MB - gitlabWebhookPayloadLimit = 25 * 1024 * 1024 -) - -type WebhookServer struct { - http *lib.HTTP - onWebhook WebhookFunc - secret string - counter uint64 -} - -func NewWebhookServer(conf lib.HTTPConfig, secret string, onWebhook WebhookFunc) (*WebhookServer, error) { - httpSrv, err := lib.NewHTTP(conf) - if err != nil { - return nil, trace.Wrap(err) - } - srv := &WebhookServer{ - http: httpSrv, - onWebhook: onWebhook, - secret: secret, - } - srv.http.POST(gitlabWebhookPath, srv.processWebhook) - return srv, nil -} - -func (s *WebhookServer) ServiceJob() lib.ServiceJob { - return s.http.ServiceJob() -} - -func (s *WebhookServer) WebhookURL() string { - return s.http.NewURL(gitlabWebhookPath, nil).String() -} - -func (s *WebhookServer) BaseURL() *url.URL { - return s.http.BaseURL() -} - -func (s *WebhookServer) EnsureCert() error { - return s.http.EnsureCert(DefaultDir + "/server") -} - -func (s *WebhookServer) processWebhook(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) { - // TODO: figure out timeout - ctx, cancel := context.WithTimeout(r.Context(), time.Second*5) - defer cancel() - - httpRequestID := fmt.Sprintf("%v-%v", time.Now().Unix(), atomic.AddUint64(&s.counter, 1)) - ctx, log := logger.WithField(ctx, "gitlab_http_id", httpRequestID) - - if contentType := r.Header.Get("Content-Type"); contentType != "application/json" { - log.Errorf(`Invalid "Content-Type" header %q`, contentType) - http.Error(rw, "", http.StatusBadRequest) - return - } - // the length of the secret token is not particularly confidential, so it's ok to leak it here - if subtle.ConstantTimeCompare([]byte(r.Header.Get("X-Gitlab-Token")), []byte(s.secret)) == 0 { - log.Error(`Invalid webhook secret provided`) - http.Error(rw, "", http.StatusUnauthorized) - return - } - - body, err := ioutil.ReadAll(io.LimitReader(r.Body, gitlabWebhookPayloadLimit+1)) - if err != nil { - log.WithError(err).Error("Failed to read webhook payload") - http.Error(rw, "", http.StatusInternalServerError) - return - } - if len(body) > gitlabWebhookPayloadLimit { - log.Error("Received a webhook larger than %d bytes", gitlabWebhookPayloadLimit) - http.Error(rw, "", http.StatusRequestEntityTooLarge) - } - - var event interface{} - switch eventType := r.Header.Get("X-Gitlab-Event"); eventType { - case "Issue Hook": - var issueEvent IssueEvent - if err = json.Unmarshal(body, &issueEvent); err != nil { - log.WithError(err).Error("Failed to parse webhook payload") - http.Error(rw, "", http.StatusBadRequest) - return - } - event = issueEvent - default: - log.Warningf(`Received unsupported hook %q`, eventType) - rw.WriteHeader(http.StatusNoContent) - return - } - - if err := s.onWebhook(ctx, Webhook{Event: event}); err != nil { - log.WithError(err).Error("Failed to process webhook") - log.Debugf("%v", trace.DebugReport(err)) - var code int - switch { - case lib.IsCanceled(err) || lib.IsDeadline(err): - code = http.StatusServiceUnavailable - default: - code = http.StatusInternalServerError - } - http.Error(rw, "", code) - return - } - - rw.WriteHeader(http.StatusNoContent) -} diff --git a/charts/access/email/README.md b/charts/access/email/README.md index 738b7f84c..938d2569f 100644 --- a/charts/access/email/README.md +++ b/charts/access/email/README.md @@ -116,7 +116,7 @@ The following values can be set for the Helm chart: log.output Logger output. Could be "stdout", "stderr" or a file name, - eg. "/var/lib/teleport/gitlab.log" + eg. "/var/lib/teleport/email.log" string "stdout" diff --git a/docker/Makefile b/docker/Makefile index 5d8267f93..f22d1fe2d 100644 --- a/docker/Makefile +++ b/docker/Makefile @@ -20,7 +20,7 @@ TELEPORT_FLAGS ?= PLUGIN_FLAGS ?= # Which plugins to run on make up? -PLUGINS ?= teleport-slack teleport-jira teleport-mattermost teleport-gitlab teleport-pagerduty teleport-webhooks +PLUGINS ?= teleport-slack teleport-jira teleport-mattermost teleport-pagerduty teleport-webhooks # # Default target starts a teleport cluster with a single node, diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index d5be8f459..fe9b27ed8 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -88,21 +88,6 @@ services: aliases: - "teleport-pagerduty.cluster.local" - teleport-gitlab: - <<: *plugin - container_name: teleport-gitlab - command: | - ${PLUGINSHOME}/access/gitlab/build/teleport-gitlab start - -c ${PLUGINSHOME}/docker/plugins/teleport-gitlab.toml - ${PLUGIN_FLAGS} - ports: - - "8044:8044" - networks: - teleport-network: - ipv4_address: 172.10.1.44 - aliases: - - "teleport-gitlab.cluster.local" - teleport-jira-cloud: <<: *plugin container_name: teleport-jira-cloud diff --git a/docker/ngrok-insecure.yaml.example b/docker/ngrok-insecure.yaml.example index 6439fdf56..6b0e1fc60 100644 --- a/docker/ngrok-insecure.yaml.example +++ b/docker/ngrok-insecure.yaml.example @@ -20,10 +20,6 @@ tunnels: addr: 8042 proto: http host_header: teleport-mattermost.cluster.local - teleport-gitlab: - addr: 8044 - proto: http - host_header: teleport-gitlab.cluster.local teleport-jira-cloud: addr: 8045 proto: http @@ -35,4 +31,4 @@ tunnels: teleport-webhooks: addr: 8046 proto: http - host_header: teleport-webhooks.cluster.local \ No newline at end of file + host_header: teleport-webhooks.cluster.local diff --git a/docker/plugins/teleport-gitlab.toml b/docker/plugins/teleport-gitlab.toml deleted file mode 100644 index 93930b03a..000000000 --- a/docker/plugins/teleport-gitlab.toml +++ /dev/null @@ -1,24 +0,0 @@ -[teleport] -auth_server = "teleport.cluster.local:3025" # Teleport Auth Server GRPC API address -client_key = "/mnt/shared/certs/access-plugin/plug.key" # Teleport GRPC client secret key -client_crt = "/mnt/shared/certs/access-plugin/plug.crt" # Teleport GRPC client certificate -root_cas = "/mnt/shared/certs/access-plugin/plug.cas" # Teleport cluster CA certs - -[db] -path = "/var/lib/teleport/plugins/gitlab/database" # Path to the database file - -[gitlab] -url = "" # Leave empty if you are using cloud -token = "token" # GitLab API Token -project_id = "1812345" # GitLab Project ID -webhook_secret = "your webhook passphrase" # A secret used to encrypt data we use in webhooks. Basically anything you'd like. - -[http] -public_addr = "teleport-gitlab.cluster.local" # URL on which callback server is accessible externally, e.g. [https://]teleport-proxy.example.com -listen_addr = ":8044" # Network address in format [addr]:port on which callback server listens, e.g. 0.0.0.0:8081 -https_key_file = "/var/lib/teleport/webproxy_key.pem" # TLS private key -https_cert_file = "/var/lib/teleport/webproxy_cert.pem" # TLS certificate - -[log] -output = "stderr" # Logger output. Could be "stdout", "stderr" or "/var/lib/teleport/gitlab.log" -severity = "INFO" # Logger severity. Could be "INFO", "ERROR", "DEBUG" or "WARN". \ No newline at end of file diff --git a/testplan.md b/testplan.md index 0fca89ef3..315d13124 100644 --- a/testplan.md +++ b/testplan.md @@ -99,21 +99,3 @@ it should be run on plugin release and with each new Teleport Release. - [ ] A long running request should gracefully degrade - [ ] Teleport Audit log displays correct user approve/deny in UI ( /audit/events ) - -### Gitlab Plugin - -- [ ] Setup has been configured using [Gitlab](https://about.gitlab.com/install/) -- [ ] Setting up Github Board and OAuth token instructions are up-to-date. -- [ ] `teleport-gitlab configure` outputs valid TOML -- [ ] Plugin started with TLS -- [ ] Plugin started --insecure-no-tls - -- [ ] End user's `tsh login --request-roles=REQUESTED_ROLE` appears in Gitlab Board -- [ ] Any Gitlab board member is able to Approve the request. -- [ ] End user now sees role approved in CLI -- [ ] Any Gitlab board member is able to Deny the request. -- [ ] End user now sees role denied in CLI - -- [ ] A long running request should gracefully degrade - -- [ ] Teleport Audit log displays correct user approve/deny in UI ( /audit/events ) \ No newline at end of file From 1cdfbb4e9d09fae10a5fcefdffcfd40c8c1c364c Mon Sep 17 00:00:00 2001 From: kazimierzbudzyk <62395833+kazimierzbudzyk@users.noreply.github.com> Date: Wed, 6 Apr 2022 16:16:10 +0100 Subject: [PATCH 07/18] improvement: allow setting terraform auth secrets through base64 encoded env vars (#476) Co-authored-by: Marco Dinis Co-authored-by: Roman Tkachenko --- lib/tctl/tctl.go | 4 +- lib/testing/integration/integration.go | 29 ++++++- terraform/provider/provider.go | 103 +++++++++++++++++++++++-- terraform/test/configuration_test.go | 89 +++++++++++++++++++++ terraform/test/main_test.go | 14 +++- 5 files changed, 227 insertions(+), 12 deletions(-) create mode 100644 terraform/test/configuration_test.go diff --git a/lib/tctl/tctl.go b/lib/tctl/tctl.go index ba0b98cc7..9abfcae53 100644 --- a/lib/tctl/tctl.go +++ b/lib/tctl/tctl.go @@ -42,7 +42,7 @@ func (tctl Tctl) CheckExecutable() error { } // Sign generates Teleport client credentials at a given path. -func (tctl Tctl) Sign(ctx context.Context, username, outPath string) error { +func (tctl Tctl) Sign(ctx context.Context, username, format, outPath string) error { log := logger.Get(ctx) args := append(tctl.baseArgs(), "auth", @@ -50,7 +50,7 @@ func (tctl Tctl) Sign(ctx context.Context, username, outPath string) error { "--user", username, "--format", - "file", + format, "--overwrite", "--out", outPath, diff --git a/lib/testing/integration/integration.go b/lib/testing/integration/integration.go index 98774757a..410568914 100644 --- a/lib/testing/integration/integration.go +++ b/lib/testing/integration/integration.go @@ -87,6 +87,12 @@ type Version struct { IsEnterprise bool } +type SignTLSPaths struct { + CertPath string + KeyPath string + RootCAPath string +} + const serviceShutdownTimeout = 10 * time.Second // New initializes a Teleport installation. @@ -420,12 +426,33 @@ func (integration *Integration) Sign(ctx context.Context, auth *AuthService, use return "", trace.Wrap(err) } outPath := outFile.Name() - if err := integration.tctl(auth).Sign(ctx, userName, outPath); err != nil { + if err := integration.tctl(auth).Sign(ctx, userName, "file", outPath); err != nil { return "", trace.Wrap(err) } return outPath, nil } +// SignTLS generates a set of files to be used for generating the TLS Config: Cert, Key and RootCAs +func (integration *Integration) SignTLS(ctx context.Context, auth *AuthService, userName string) (*SignTLSPaths, error) { + outFile, err := integration.tempFile(fmt.Sprintf("credentials-%s-*", userName)) + if err != nil { + return nil, trace.Wrap(err) + } + if err := outFile.Close(); err != nil { + return nil, trace.Wrap(err) + } + outPath := outFile.Name() + if err := integration.tctl(auth).Sign(ctx, userName, "tls", outPath); err != nil { + return nil, trace.Wrap(err) + } + + return &SignTLSPaths{ + CertPath: outPath + ".crt", + KeyPath: outPath + ".key", + RootCAPath: outPath + ".cas", + }, nil +} + // SetCAPin sets integration with the auth service's CA Pin. func (integration *Integration) SetCAPin(ctx context.Context, auth *AuthService) error { if integration.caPin != "" { diff --git a/terraform/provider/provider.go b/terraform/provider/provider.go index 4bc3cf4a8..1c8c5b3ec 100644 --- a/terraform/provider/provider.go +++ b/terraform/provider/provider.go @@ -18,6 +18,9 @@ package provider import ( "context" + "crypto/tls" + "crypto/x509" + "encoding/base64" "fmt" "net" "os" @@ -52,10 +55,16 @@ type providerData struct { Addr types.String `tfsdk:"addr"` // CertPath path to TLS certificate file CertPath types.String `tfsdk:"cert_path"` + // CertBase64 base64 encoded TLS certificate file + CertBase64 types.String `tfsdk:"cert_base64"` // KeyPath path to TLS private key file KeyPath types.String `tfsdk:"key_path"` + // KeyBase64 base64 encoded TLS private key + KeyBase64 types.String `tfsdk:"key_base64"` // RootCAPath path to TLS root CA certificate file RootCaPath types.String `tfsdk:"root_ca_path"` + // RootCaPath base64 encoded root CA certificate + RootCaBase64 types.String `tfsdk:"root_ca_base64"` // ProfileName Teleport profile name ProfileName types.String `tfsdk:"profile_name"` // ProfileDir Teleport profile dir @@ -85,36 +94,52 @@ func (p *Provider) GetSchema(_ context.Context) (tfsdk.Schema, diag.Diagnostics) Optional: true, Description: "Path to Teleport auth certificate file.", }, + "cert_base64": { + Type: types.StringType, + Optional: true, + Description: "Base64 encoded TLS auth certificate.", + }, "key_path": { Type: types.StringType, Optional: true, Description: "Path to Teleport auth key file.", }, + "key_base64": { + Type: types.StringType, + Sensitive: true, + Optional: true, + Description: "Base64 encoded TLS auth key.", + }, "root_ca_path": { Type: types.StringType, Optional: true, - Description: "Path to Teleport Root CA", + Description: "Path to Teleport Root CA.", + }, + "root_ca_base64": { + Type: types.StringType, + Optional: true, + Description: "Base64 encoded Root CA.", }, "profile_name": { Type: types.StringType, Optional: true, - Description: "Teleport profile name", + Description: "Teleport profile name.", }, "profile_dir": { Type: types.StringType, Optional: true, - Description: "Teleport profile path", + Description: "Teleport profile path.", }, "identity_file_path": { Type: types.StringType, Optional: true, - Description: "Teleport identity file path", + Description: "Teleport identity file path.", }, "identity_file": { Type: types.StringType, Sensitive: true, Optional: true, - Description: "Teleport identity file content", + Description: "Teleport identity file content.", }, }, }, nil @@ -147,8 +172,11 @@ func (p *Provider) Configure(ctx context.Context, req tfsdk.ConfigureProviderReq addr := p.stringFromConfigOrEnv(config.Addr, "TF_TELEPORT_ADDR", "") certPath := p.stringFromConfigOrEnv(config.CertPath, "TF_TELEPORT_CERT", "") + certBase64 := p.stringFromConfigOrEnv(config.CertBase64, "TF_TELEPORT_CERT_BASE64", "") keyPath := p.stringFromConfigOrEnv(config.KeyPath, "TF_TELEPORT_KEY", "") + keyBase64 := p.stringFromConfigOrEnv(config.KeyBase64, "TF_TELEPORT_KEY_BASE64", "") caPath := p.stringFromConfigOrEnv(config.RootCaPath, "TF_TELEPORT_ROOT_CA", "") + caBase64 := p.stringFromConfigOrEnv(config.RootCaBase64, "TF_TELEPORT_CA_BASE64", "") profileName := p.stringFromConfigOrEnv(config.ProfileName, "TF_TELEPORT_PROFILE_NAME", "") profileDir := p.stringFromConfigOrEnv(config.ProfileDir, "TF_TELEPORT_PROFILE_PATH", "") identityFilePath := p.stringFromConfigOrEnv(config.IdentityFilePath, "TF_TELEPORT_IDENTITY_FILE_PATH", "") @@ -162,7 +190,7 @@ func (p *Provider) Configure(ctx context.Context, req tfsdk.ConfigureProviderReq if certPath != "" && keyPath != "" { l := log.WithField("cert_path", certPath).WithField("key_path", keyPath).WithField("root_ca_path", caPath) - l.Debug("Using auth with certificate, private key and (optionally) CA") + l.Debug("Using auth with certificate, private key and (optionally) CA read from files") cred, ok := p.getCredentialsFromKeyPair(certPath, keyPath, caPath, resp) if !ok { @@ -171,6 +199,15 @@ func (p *Provider) Configure(ctx context.Context, req tfsdk.ConfigureProviderReq creds = append(creds, cred) } + if certBase64 != "" && keyBase64 != "" { + log.Debug("Using auth with certificate, private key and (optionally) CA read from base64 encoded vars") + cred, ok := p.getCredentialsFromBase64(certBase64, keyBase64, caBase64, resp) + if !ok { + return + } + creds = append(creds, cred) + } + if identityFilePath != "" { log.WithField("identity_file_path", identityFilePath).Debug("Using auth with identity file") @@ -246,7 +283,7 @@ func (p *Provider) stringFromConfigOrEnv(value types.String, env string, def str if value.Unknown || value.Null { value := os.Getenv(env) if value != "" { - return def + return value } } @@ -279,6 +316,43 @@ func (p *Provider) validateAddr(addr string, resp *tfsdk.ConfigureProviderRespon return true } +// getCredentialsFromBase64 returns client.Credentials built from base64 encoded keys +func (p *Provider) getCredentialsFromBase64(certBase64, keyBase64, caBase64 string, resp *tfsdk.ConfigureProviderResponse) (client.Credentials, bool) { + cert, err := base64.StdEncoding.DecodeString(certBase64) + if err != nil { + resp.Diagnostics.AddError( + "Failed to base64 decode cert", + fmt.Sprintf("Please check if cert_base64 (or TF_TELEPORT_CERT_BASE64) is set correctly. Error: %s", err), + ) + return nil, false + } + key, err := base64.StdEncoding.DecodeString(keyBase64) + if err != nil { + resp.Diagnostics.AddError( + "Failed to base64 decode key", + fmt.Sprintf("Please check if key_base64 (or TF_TELEPORT_KEY_BASE64) is set correctly. Error: %s", err), + ) + return nil, false + } + rootCa, err := base64.StdEncoding.DecodeString(caBase64) + if err != nil { + resp.Diagnostics.AddError( + "Failed to base64 decode root ca", + fmt.Sprintf("Please check if root_ca_base64 (or TF_TELEPORT_CA_BASE64) is set correctly. Error: %s", err), + ) + return nil, false + } + tlsConfig, err := createTLSConfig(cert, key, rootCa) + if err != nil { + resp.Diagnostics.AddError( + "Failed to create TLS config", + fmt.Sprintf("Error: %s", err), + ) + return nil, false + } + return client.LoadTLS(tlsConfig), true +} + // getCredentialsFromKeyPair returns client.Credentials built from path to key files func (p *Provider) getCredentialsFromKeyPair(certPath string, keyPath string, caPath string, resp *tfsdk.ConfigureProviderResponse) (client.Credentials, bool) { if !p.fileExists(certPath) { @@ -348,6 +422,21 @@ func (p *Provider) configureLog() { } } +// createTLSConfig returns tls.Config build from keys +func createTLSConfig(cert, key, rootCa []byte) (*tls.Config, error) { + keyPair, err := tls.X509KeyPair(cert, key) + if err != nil { + return nil, err + } + caCertPool := x509.NewCertPool() + caCertPool.AppendCertsFromPEM(rootCa) + + return &tls.Config{ + Certificates: []tls.Certificate{keyPair}, + RootCAs: caCertPool, + }, nil +} + // GetResources returns the map of provider resources func (p *Provider) GetResources(_ context.Context) (map[string]tfsdk.ResourceType, diag.Diagnostics) { return map[string]tfsdk.ResourceType{ diff --git a/terraform/test/configuration_test.go b/terraform/test/configuration_test.go new file mode 100644 index 000000000..dd33cc912 --- /dev/null +++ b/terraform/test/configuration_test.go @@ -0,0 +1,89 @@ +/* +Copyright 2022 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 test + +import ( + "encoding/base64" + "os" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/stretchr/testify/require" +) + +func (s *TerraformSuite) TestConfigureAuthBase64() { + name := "teleport_app.test" + + key, err := os.ReadFile(s.teleportConfig.ClientKey) + require.NoError(s.T(), err) + keyBase64 := base64.StdEncoding.EncodeToString(key) + + cert, err := os.ReadFile(s.teleportConfig.ClientCrt) + require.NoError(s.T(), err) + certBase64 := base64.StdEncoding.EncodeToString(cert) + + rootCA, err := os.ReadFile(s.teleportConfig.RootCAs) + require.NoError(s.T(), err) + rootCABase64 := base64.StdEncoding.EncodeToString(rootCA) + + providerConfigUsingB64Auth := ` +provider "teleport" { + addr = "` + s.teleportConfig.Addr + `" + key_base64 = "` + keyBase64 + `" + cert_base64 = "` + certBase64 + `" + root_ca_base64 = "` + rootCABase64 + `" +} + ` + + resource.Test(s.T(), resource.TestCase{ + ProtoV6ProviderFactories: s.terraformProviders, + Steps: []resource.TestStep{ + { + Config: s.getFixtureWithCustomConfig("app_0_create.tf", providerConfigUsingB64Auth), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(name, "kind", "app"), + resource.TestCheckResourceAttr(name, "spec.uri", "localhost:3000"), + ), + }, + }, + }) +} + +func (s *TerraformSuite) TestConfigureAuthFiles() { + name := "teleport_app.test" + + providerConfigUsingAuthFiles := ` +provider "teleport" { + addr = "` + s.teleportConfig.Addr + `" + key_path = "` + s.teleportConfig.ClientKey + `" + cert_path = "` + s.teleportConfig.ClientCrt + `" + root_ca_path = "` + s.teleportConfig.RootCAs + `" +} + ` + + resource.Test(s.T(), resource.TestCase{ + ProtoV6ProviderFactories: s.terraformProviders, + Steps: []resource.TestStep{ + { + Config: s.getFixtureWithCustomConfig("app_0_create.tf", providerConfigUsingAuthFiles), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(name, "kind", "app"), + resource.TestCheckResourceAttr(name, "spec.uri", "localhost:3000"), + ), + }, + }, + }) +} diff --git a/terraform/test/main_test.go b/terraform/test/main_test.go index 163bf6a35..ab1de2fe0 100644 --- a/terraform/test/main_test.go +++ b/terraform/test/main_test.go @@ -112,11 +112,17 @@ func (s *TerraformSuite) SetupSuite() { identityPath, err := s.Integration.Sign(ctx, s.Auth, s.plugin) require.NoError(t, err) + tlsPaths, err := s.Integration.SignTLS(ctx, s.Auth, s.plugin) + require.NoError(t, err) + s.client, err = s.Integration.NewClient(ctx, s.Auth, s.plugin) require.NoError(t, err) s.teleportConfig.Addr = s.Auth.AuthAddr().String() s.teleportConfig.Identity = identityPath + s.teleportConfig.ClientCrt = tlsPaths.CertPath + s.teleportConfig.ClientKey = tlsPaths.KeyPath + s.teleportConfig.RootCAs = tlsPaths.RootCAPath s.teleportFeatures = teleportFeatures s.terraformConfig = ` @@ -156,10 +162,14 @@ func (s *TerraformSuite) closeClient() { // getFixture loads fixture and returns it as string or if failed func (s *TerraformSuite) getFixture(name string) string { + return s.getFixtureWithCustomConfig(name, s.terraformConfig) +} + +// getFixtureWithCustomConfig loads fixture and returns it as string or if failed +func (s *TerraformSuite) getFixtureWithCustomConfig(name string, config string) string { b, err := fixtures.ReadFile(filepath.Join("fixtures", name)) if err != nil { return fmt.Sprintf("", name) } - - return s.terraformConfig + "\n" + string(b) + return config + "\n" + string(b) } From 8007a13dfaf5ea973b7f0b6f4001febcb375161e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Smoli=C5=84ski?= Date: Thu, 7 Apr 2022 01:52:30 +0200 Subject: [PATCH 08/18] Update Teleport API module dependency to v9.0.4 (#480) --- go.mod | 4 +--- go.sum | 7 ++----- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index 4d937cd82..d55d159d9 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/google/go-querystring v1.0.0 github.com/google/uuid v1.2.0 github.com/gravitational/kingpin v2.1.11-0.20190130013101-742f2714c145+incompatible - github.com/gravitational/teleport/api v0.0.0-20220317023058-7bbe6f15c5c8 // tag v9.0.1 + github.com/gravitational/teleport/api v0.0.0-20220406233052-f577413d3c2a // tag v9.0.4 github.com/gravitational/trace v1.1.17 github.com/hashicorp/go-version v1.3.0 github.com/hashicorp/terraform-plugin-framework v0.6.1 @@ -33,7 +33,6 @@ require ( github.com/sethvargo/go-limiter v0.7.2 github.com/sirupsen/logrus v1.8.1 github.com/stretchr/testify v1.7.0 - go.etcd.io/bbolt v1.3.5 golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 golang.org/x/tools v0.0.0-20210106214847-113979e3529a // indirect @@ -42,7 +41,6 @@ require ( google.golang.org/protobuf v1.27.1 gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/mail.v2 v2.3.1 - gopkg.in/resty.v1 v1.12.0 k8s.io/apimachinery v0.20.4 ) diff --git a/go.sum b/go.sum index df30eb198..75d221653 100644 --- a/go.sum +++ b/go.sum @@ -255,8 +255,8 @@ github.com/gravitational/kingpin v2.1.11-0.20190130013101-742f2714c145+incompati github.com/gravitational/kingpin v2.1.11-0.20190130013101-742f2714c145+incompatible/go.mod h1:LWxG30M3FcrjhOn3T4zz7JmBoQJ45MWZmOXgy9Ganoc= github.com/gravitational/protobuf v1.3.2-0.20201123192827-2b9fcfaffcbf h1:MQ4e8XcxvZTeuOmRl7yE519vcWc2h/lyvYzsvt41cdY= github.com/gravitational/protobuf v1.3.2-0.20201123192827-2b9fcfaffcbf/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= -github.com/gravitational/teleport/api v0.0.0-20220317023058-7bbe6f15c5c8 h1:DiTkx0vquZMgap4FpUCZ/jbxtDXiWNbh3NLU7KX62Tk= -github.com/gravitational/teleport/api v0.0.0-20220317023058-7bbe6f15c5c8/go.mod h1:HqUVv4V/Ln+Oix0ZIwDBhV6aMThBZD7W6mopw8yMRjs= +github.com/gravitational/teleport/api v0.0.0-20220406233052-f577413d3c2a h1:o6s288gPyvhrpcheXq+s89TPfJcqlOO+t/J6pEOkZNU= +github.com/gravitational/teleport/api v0.0.0-20220406233052-f577413d3c2a/go.mod h1:HqUVv4V/Ln+Oix0ZIwDBhV6aMThBZD7W6mopw8yMRjs= github.com/gravitational/trace v1.1.17 h1:BkF30oLm1aKMZ5SPVbnlVbYtYEsG26zHxA4dJ+Z46dM= github.com/gravitational/trace v1.1.17/go.mod h1:n0ijrq6psJY0sOI/NzLp+xdd8xl79jjwzVOFHDY6+kQ= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= @@ -530,8 +530,6 @@ github.com/zclconf/go-cty v1.9.1 h1:viqrgQwFl5UpSxc046qblj78wZXVDFnSOufaOTER+cc= github.com/zclconf/go-cty v1.9.1/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk= github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0= -go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= @@ -871,7 +869,6 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk= gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw= -gopkg.in/resty.v1 v1.12.0 h1:CuXP0Pjfw9rOuY6EP+UvtNvt5DSqHpIxILZKT/quCZI= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= From a532a50d47fdd26704ccd05f19891814301cf10c Mon Sep 17 00:00:00 2001 From: Roman Tkachenko Date: Wed, 6 Apr 2022 18:01:36 -0700 Subject: [PATCH 09/18] Release 9.0.4 (#487) --- access/email/Makefile | 2 +- access/jira/Makefile | 2 +- access/mattermost/Makefile | 2 +- access/pagerduty/Makefile | 2 +- access/slack/Makefile | 2 +- event-handler/Makefile | 2 +- terraform/install.mk | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/access/email/Makefile b/access/email/Makefile index 02e97c2b4..202261d8a 100644 --- a/access/email/Makefile +++ b/access/email/Makefile @@ -1,4 +1,4 @@ -VERSION=9.0.3 +VERSION=9.0.4 GO_VERSION=1.17.8 BUILDDIR ?= build diff --git a/access/jira/Makefile b/access/jira/Makefile index 52e3eb72e..19ef4e15d 100644 --- a/access/jira/Makefile +++ b/access/jira/Makefile @@ -1,4 +1,4 @@ -VERSION=9.0.3 +VERSION=9.0.4 GO_VERSION=1.17.8 BUILDDIR ?= build diff --git a/access/mattermost/Makefile b/access/mattermost/Makefile index eebbaff9b..31d0d968e 100644 --- a/access/mattermost/Makefile +++ b/access/mattermost/Makefile @@ -1,4 +1,4 @@ -VERSION=9.0.3 +VERSION=9.0.4 GO_VERSION=1.17.8 BUILDDIR ?= build diff --git a/access/pagerduty/Makefile b/access/pagerduty/Makefile index d4ff6ec60..b85809473 100644 --- a/access/pagerduty/Makefile +++ b/access/pagerduty/Makefile @@ -1,4 +1,4 @@ -VERSION=9.0.3 +VERSION=9.0.4 GO_VERSION=1.17.8 BUILDDIR ?= build diff --git a/access/slack/Makefile b/access/slack/Makefile index 139917327..56ce2b88b 100644 --- a/access/slack/Makefile +++ b/access/slack/Makefile @@ -1,4 +1,4 @@ -VERSION=9.0.3 +VERSION=9.0.4 GO_VERSION=1.17.8 BUILDDIR ?= build diff --git a/event-handler/Makefile b/event-handler/Makefile index c71d0dba2..56ad0c3f7 100644 --- a/event-handler/Makefile +++ b/event-handler/Makefile @@ -1,4 +1,4 @@ -VERSION=9.0.3 +VERSION=9.0.4 GO_VERSION=1.17.8 OS ?= $(shell go env GOOS) diff --git a/terraform/install.mk b/terraform/install.mk index 8d92feeb0..4c3d62378 100644 --- a/terraform/install.mk +++ b/terraform/install.mk @@ -1,4 +1,4 @@ -VERSION=9.0.3 +VERSION=9.0.4 OS ?= $(shell go env GOOS) ARCH ?= $(shell go env GOARCH) From 706688af0358de2d6e810086159b362aecf25a5a Mon Sep 17 00:00:00 2001 From: Logan Davis <38335829+logand22@users.noreply.github.com> Date: Tue, 12 Apr 2022 15:04:33 -0500 Subject: [PATCH 10/18] Update OCI image names (#489) --- access/email/Makefile | 4 ++-- access/email/README.md | 8 ++++---- access/jira/Makefile | 4 ++-- access/jira/README.md | 8 ++++---- access/mattermost/Makefile | 4 ++-- access/mattermost/README.md | 8 ++++---- access/pagerduty/Makefile | 4 ++-- access/pagerduty/README.md | 8 ++++---- access/slack/Makefile | 4 ++-- access/slack/README.md | 8 ++++---- charts/access/email/Chart.yaml | 2 +- .../__snapshot__/configmap_test.yaml.snap | 18 +++++++++--------- .../__snapshot__/deployment_test.yaml.snap | 14 +++++++------- charts/access/email/values.schema.json | 6 +++--- charts/access/email/values.yaml | 2 +- event-handler/Makefile | 4 ++-- event-handler/README.md | 8 ++++---- 17 files changed, 57 insertions(+), 57 deletions(-) diff --git a/access/email/Makefile b/access/email/Makefile index 202261d8a..8117a85a3 100644 --- a/access/email/Makefile +++ b/access/email/Makefile @@ -16,8 +16,8 @@ RELEASE=$(RELEASE_NAME)-$(GITTAG)-$(OS)-$(ARCH)-bin RELEASE_MESSAGE = "Building with GOOS=$(OS) GOARCH=$(ARCH)." -DOCKER_NAME = access-plugin-email -DOCKER_IMAGE = 146628656107.dkr.ecr.us-west-2.amazonaws.com/teleport/$(DOCKER_NAME):$(VERSION) +DOCKER_NAME = teleport-plugin-email +DOCKER_IMAGE = 146628656107.dkr.ecr.us-west-2.amazonaws.com/gravitational/$(DOCKER_NAME):$(VERSION) DOCKER_IMAGE_QUAY = quay.io/gravitational/$(DOCKER_NAME):$(VERSION) DOCKER_BUILD_ARGS = --build-arg GO_VERSION=${GO_VERSION} --build-arg ACCESS_PLUGIN=email --build-arg GITREF=$(GITREF) diff --git a/access/email/README.md b/access/email/README.md index bf2dc5010..6a2790501 100644 --- a/access/email/README.md +++ b/access/email/README.md @@ -23,15 +23,15 @@ $ ./install ### Docker Image ```bash -$ docker pull quay.io/gravitational/access-plugin-email:9.0.2 +$ docker pull quay.io/gravitational/teleport-plugin-email:9.0.2 ``` ```bash -$ docker run quay.io/gravitational/access-plugin-email:9.0.2 version +$ docker run quay.io/gravitational/teleport-plugin-email:9.0.2 version teleport-email v9.0.2 git:teleport-email-v9.0.2-0-g9e149895 go1.17.8 ``` -For a list of available tags, visit [https://quay.io/](https://quay.io/repository/gravitational/access-plugin-email?tab=tags) +For a list of available tags, visit [https://quay.io/](https://quay.io/repository/gravitational/teleport-plugin-email?tab=tags) ### Building from source @@ -126,7 +126,7 @@ $ teleport-email start or with docker: ```bash -$ docker run -v :/etc/teleport-email.toml quay.io/gravitational/access-plugin-email:9.0.2 start +$ docker run -v :/etc/teleport-email.toml quay.io/gravitational/teleport-plugin-email:9.0.2 start ``` If something bad happens, try to run it with `-d` option i.e. `teleport-email start -d` and attach the stdout output to the issue you are going to create. diff --git a/access/jira/Makefile b/access/jira/Makefile index 19ef4e15d..533a25db1 100644 --- a/access/jira/Makefile +++ b/access/jira/Makefile @@ -16,8 +16,8 @@ RELEASE=$(RELEASE_NAME)-$(GITTAG)-$(OS)-$(ARCH)-bin RELEASE_MESSAGE = "Building with GOOS=$(OS) GOARCH=$(ARCH)." -DOCKER_NAME = access-plugin-jira -DOCKER_IMAGE = 146628656107.dkr.ecr.us-west-2.amazonaws.com/teleport/$(DOCKER_NAME):$(VERSION) +DOCKER_NAME = teleport-plugin-jira +DOCKER_IMAGE = 146628656107.dkr.ecr.us-west-2.amazonaws.com/gravitational/$(DOCKER_NAME):$(VERSION) DOCKER_IMAGE_QUAY = quay.io/gravitational/$(DOCKER_NAME):$(VERSION) DOCKER_BUILD_ARGS = --build-arg GO_VERSION=${GO_VERSION} --build-arg ACCESS_PLUGIN=jira --build-arg GITREF=$(GITREF) diff --git a/access/jira/README.md b/access/jira/README.md index 8c80107df..cb50cfd23 100644 --- a/access/jira/README.md +++ b/access/jira/README.md @@ -27,15 +27,15 @@ $ ./install ### Docker Image ```bash -$ docker pull quay.io/gravitational/access-plugin-jira:9.0.2 +$ docker pull quay.io/gravitational/teleport-plugin-jira:9.0.2 ``` ```bash -$ docker run quay.io/gravitational/access-plugin-jira:9.0.2 version +$ docker run quay.io/gravitational/teleport-plugin-jira:9.0.2 version teleport-jira v9.0.2 git:teleport-jira-v9.0.2-0-g9e149895 go1.17.8 ``` -For a list of available tags, visit [https://quay.io/](https://quay.io/repository/gravitational/access-plugin-jira?tab=tags) +For a list of available tags, visit [https://quay.io/](https://quay.io/repository/gravitational/teleport-plugin-jira?tab=tags) ### Building from source @@ -153,7 +153,7 @@ $ teleport-jira start or with docker: ```bash -$ docker run -v :/etc/teleport-jira.toml quay.io/gravitational/access-plugin-jira:9.0.2 start +$ docker run -v :/etc/teleport-jira.toml quay.io/gravitational/teleport-plugin-jira:9.0.2 start ``` If something bad happens, try to run it with `-d` option i.e. `teleport-jira start -d` and attach the stdout output to the issue you are going to create. diff --git a/access/mattermost/Makefile b/access/mattermost/Makefile index 31d0d968e..c1c7d19a4 100644 --- a/access/mattermost/Makefile +++ b/access/mattermost/Makefile @@ -16,8 +16,8 @@ RELEASE=$(RELEASE_NAME)-$(GITTAG)-$(OS)-$(ARCH)-bin RELEASE_MESSAGE = "Building with GOOS=$(OS) GOARCH=$(ARCH)." -DOCKER_NAME=access-plugin-mattermost -DOCKER_IMAGE = 146628656107.dkr.ecr.us-west-2.amazonaws.com/teleport/$(DOCKER_NAME):$(VERSION) +DOCKER_NAME = teleport-plugin-mattermost +DOCKER_IMAGE = 146628656107.dkr.ecr.us-west-2.amazonaws.com/gravitational/$(DOCKER_NAME):$(VERSION) DOCKER_IMAGE_QUAY = quay.io/gravitational/$(DOCKER_NAME):$(VERSION) DOCKER_BUILD_ARGS = --build-arg GO_VERSION=${GO_VERSION} --build-arg ACCESS_PLUGIN=mattermost --build-arg GITREF=$(GITREF) diff --git a/access/mattermost/README.md b/access/mattermost/README.md index 3f22ed22e..de6f9fd8f 100644 --- a/access/mattermost/README.md +++ b/access/mattermost/README.md @@ -36,15 +36,15 @@ $ ./install ### Docker Image ```bash -$ docker pull quay.io/gravitational/access-plugin-mattermost:9.0.2 +$ docker pull quay.io/gravitational/teleport-plugin-mattermost:9.0.2 ``` ```bash -$ docker run quay.io/gravitational/access-plugin-mattermost:9.0.2 version +$ docker run quay.io/gravitational/teleport-plugin-mattermost:9.0.2 version teleport-mattermost v9.0.2 git:teleport-mattermost-v9.0.2-0-g9e149895 go1.17.8 ``` -For a list of available tags, visit [https://quay.io/](https://quay.io/repository/gravitational/access-plugin-mattermost?tab=tags) +For a list of available tags, visit [https://quay.io/](https://quay.io/repository/gravitational/teleport-plugin-mattermost?tab=tags) ### Building from source @@ -171,7 +171,7 @@ $ teleport-mattermost start or with docker: ```bash -$ docker run -v :/etc/teleport-mattermost.toml quay.io/gravitational/access-plugin-mattermost:9.0.2 start +$ docker run -v :/etc/teleport-mattermost.toml quay.io/gravitational/teleport-plugin-mattermost:9.0.2 start ``` ## The Workflow diff --git a/access/pagerduty/Makefile b/access/pagerduty/Makefile index b85809473..3f5046a30 100644 --- a/access/pagerduty/Makefile +++ b/access/pagerduty/Makefile @@ -16,8 +16,8 @@ RELEASE=$(RELEASE_NAME)-$(GITTAG)-$(OS)-$(ARCH)-bin RELEASE_MESSAGE = "Building with GOOS=$(OS) GOARCH=$(ARCH)." -DOCKER_NAME=access-plugin-pagerduty -DOCKER_IMAGE = 146628656107.dkr.ecr.us-west-2.amazonaws.com/teleport/$(DOCKER_NAME):$(VERSION) +DOCKER_NAME = teleport-plugin-pagerduty +DOCKER_IMAGE = 146628656107.dkr.ecr.us-west-2.amazonaws.com/gravitational/$(DOCKER_NAME):$(VERSION) DOCKER_IMAGE_QUAY = quay.io/gravitational/$(DOCKER_NAME):$(VERSION) DOCKER_BUILD_ARGS = --build-arg GO_VERSION=${GO_VERSION} --build-arg ACCESS_PLUGIN=pagerduty --build-arg GITREF=$(GITREF) diff --git a/access/pagerduty/README.md b/access/pagerduty/README.md index ebe27469a..1e2eefb4f 100644 --- a/access/pagerduty/README.md +++ b/access/pagerduty/README.md @@ -37,15 +37,15 @@ $ ./install ### Docker Image ```bash -$ docker pull quay.io/gravitational/access-plugin-pagerduty:9.0.2 +$ docker pull quay.io/gravitational/teleport-plugin-pagerduty:9.0.2 ``` ```bash -$ docker run quay.io/gravitational/access-plugin-pagerduty:9.0.2 version +$ docker run quay.io/gravitational/teleport-plugin-pagerduty:9.0.2 version teleport-pagerduty v9.0.2 git:teleport-pagerduty-v9.0.2-0-g9e149895 go1.17.8 ``` -For a list of available tags, visit [https://quay.io/](https://quay.io/repository/gravitational/access-plugin-pagerduty?tab=tags) +For a list of available tags, visit [https://quay.io/](https://quay.io/repository/gravitational/teleport-plugin-pagerduty?tab=tags) ### Building from source @@ -198,5 +198,5 @@ $ teleport-pagerduty start or with docker: ```bash -$ docker run -v :/etc/teleport-pagerduty.toml quay.io/gravitational/access-plugin-pagerduty:9.0.2 start +$ docker run -v :/etc/teleport-pagerduty.toml quay.io/gravitational/teleport-plugin-pagerduty:9.0.2 start ``` diff --git a/access/slack/Makefile b/access/slack/Makefile index 56ce2b88b..deec67e8f 100644 --- a/access/slack/Makefile +++ b/access/slack/Makefile @@ -16,8 +16,8 @@ RELEASE=$(RELEASE_NAME)-$(GITTAG)-$(OS)-$(ARCH)-bin RELEASE_MESSAGE = "Building with GOOS=$(OS) GOARCH=$(ARCH)." -DOCKER_NAME=access-plugin-slack -DOCKER_IMAGE = 146628656107.dkr.ecr.us-west-2.amazonaws.com/teleport/$(DOCKER_NAME):$(VERSION) +DOCKER_NAME = teleport-plugin-slack +DOCKER_IMAGE = 146628656107.dkr.ecr.us-west-2.amazonaws.com/gravitational/$(DOCKER_NAME):$(VERSION) DOCKER_IMAGE_QUAY = quay.io/gravitational/$(DOCKER_NAME):$(VERSION) DOCKER_BUILD_ARGS = --build-arg GO_VERSION=${GO_VERSION} --build-arg ACCESS_PLUGIN=slack --build-arg GITREF=$(GITREF) diff --git a/access/slack/README.md b/access/slack/README.md index 4f55b349e..4b271baad 100644 --- a/access/slack/README.md +++ b/access/slack/README.md @@ -31,15 +31,15 @@ $ ./install ### Docker Image ```bash -$ docker pull quay.io/gravitational/access-plugin-slack:9.0.2 +$ docker pull quay.io/gravitational/teleport-plugin-slack:9.0.2 ``` ```bash -$ docker run quay.io/gravitational/access-plugin-slack:9.0.2 version +$ docker run quay.io/gravitational/teleport-plugin-slack:9.0.2 version teleport-slack v9.0.2 git:teleport-slack-v9.0.2-0-g9e149895 go1.17.8 ``` -For a list of available tags, visit [https://quay.io/](https://quay.io/repository/gravitational/access-plugin-slack?tab=tags) +For a list of available tags, visit [https://quay.io/](https://quay.io/repository/gravitational/teleport-plugin-slack?tab=tags) ### Building from source @@ -154,7 +154,7 @@ $ teleport-slack start or with docker: ```bash -$ docker run -v :/etc/teleport-slack.toml quay.io/gravitational/access-plugin-slack:9.0.2 start +$ docker run -v :/etc/teleport-slack.toml quay.io/gravitational/teleport-plugin-slack:9.0.2 start ``` ## Usage diff --git a/charts/access/email/Chart.yaml b/charts/access/email/Chart.yaml index 1dacf3529..e31707429 100644 --- a/charts/access/email/Chart.yaml +++ b/charts/access/email/Chart.yaml @@ -1,5 +1,5 @@ apiVersion: v2 -name: access-plugin-email +name: teleport-plugin-email description: A Helm chart for Kubernetes # A chart can be either an 'application' or a 'library' chart. diff --git a/charts/access/email/tests/__snapshot__/configmap_test.yaml.snap b/charts/access/email/tests/__snapshot__/configmap_test.yaml.snap index 6fa891e46..3a5061bf1 100644 --- a/charts/access/email/tests/__snapshot__/configmap_test.yaml.snap +++ b/charts/access/email/tests/__snapshot__/configmap_test.yaml.snap @@ -23,10 +23,10 @@ should match the snapshot (mailgun on): labels: app.kubernetes.io/instance: RELEASE-NAME app.kubernetes.io/managed-by: Helm - app.kubernetes.io/name: access-plugin-email + app.kubernetes.io/name: teleport-plugin-email app.kubernetes.io/version: 9.0.0 - helm.sh/chart: access-plugin-email-1.0.0 - name: RELEASE-NAME-access-plugin-email + helm.sh/chart: teleport-plugin-email-1.0.0 + name: RELEASE-NAME-teleport-plugin-email should match the snapshot (smtp on): 1: | apiVersion: v1 @@ -55,10 +55,10 @@ should match the snapshot (smtp on): labels: app.kubernetes.io/instance: RELEASE-NAME app.kubernetes.io/managed-by: Helm - app.kubernetes.io/name: access-plugin-email + app.kubernetes.io/name: teleport-plugin-email app.kubernetes.io/version: 9.0.0 - helm.sh/chart: access-plugin-email-1.0.0 - name: RELEASE-NAME-access-plugin-email + helm.sh/chart: teleport-plugin-email-1.0.0 + name: RELEASE-NAME-teleport-plugin-email should match the snapshot (smtp on, password file): 1: | apiVersion: v1 @@ -87,7 +87,7 @@ should match the snapshot (smtp on, password file): labels: app.kubernetes.io/instance: RELEASE-NAME app.kubernetes.io/managed-by: Helm - app.kubernetes.io/name: access-plugin-email + app.kubernetes.io/name: teleport-plugin-email app.kubernetes.io/version: 9.0.0 - helm.sh/chart: access-plugin-email-1.0.0 - name: RELEASE-NAME-access-plugin-email + helm.sh/chart: teleport-plugin-email-1.0.0 + name: RELEASE-NAME-teleport-plugin-email diff --git a/charts/access/email/tests/__snapshot__/deployment_test.yaml.snap b/charts/access/email/tests/__snapshot__/deployment_test.yaml.snap index 52d34456e..10e5482db 100644 --- a/charts/access/email/tests/__snapshot__/deployment_test.yaml.snap +++ b/charts/access/email/tests/__snapshot__/deployment_test.yaml.snap @@ -6,21 +6,21 @@ should match the snapshot: labels: app.kubernetes.io/instance: RELEASE-NAME app.kubernetes.io/managed-by: Helm - app.kubernetes.io/name: access-plugin-email + app.kubernetes.io/name: teleport-plugin-email app.kubernetes.io/version: 9.0.0 - helm.sh/chart: access-plugin-email-1.0.0 - name: RELEASE-NAME-access-plugin-email + helm.sh/chart: teleport-plugin-email-1.0.0 + name: RELEASE-NAME-teleport-plugin-email spec: replicas: 1 selector: matchLabels: app.kubernetes.io/instance: RELEASE-NAME - app.kubernetes.io/name: access-plugin-email + app.kubernetes.io/name: teleport-plugin-email template: metadata: labels: app.kubernetes.io/instance: RELEASE-NAME - app.kubernetes.io/name: access-plugin-email + app.kubernetes.io/name: teleport-plugin-email spec: containers: - command: @@ -30,7 +30,7 @@ should match the snapshot: - /etc/teleport-email.toml image: gcr.io/overridden/repository:v98.76.54 imagePullPolicy: IfNotPresent - name: access-plugin-email + name: teleport-plugin-email ports: - containerPort: 80 name: http @@ -48,7 +48,7 @@ should match the snapshot: volumes: - configMap: defaultMode: 384 - name: RELEASE-NAME-access-plugin-email + name: RELEASE-NAME-teleport-plugin-email name: config - name: auth-id secret: diff --git a/charts/access/email/values.schema.json b/charts/access/email/values.schema.json index 847be26f1..570c4d91a 100644 --- a/charts/access/email/values.schema.json +++ b/charts/access/email/values.schema.json @@ -26,7 +26,7 @@ "default": {}, "examples": [ { - "repository": "146628656107.dkr.ecr.us-west-2.amazonaws.com/teleport/access-plugin-email", + "repository": "146628656107.dkr.ecr.us-west-2.amazonaws.com/gravitational/teleport-plugin-email", "pullPolicy": "IfNotPresent", "tag": "" } @@ -40,9 +40,9 @@ "repository": { "$id": "#/properties/image/properties/repository", "type": "string", - "default": "146628656107.dkr.ecr.us-west-2.amazonaws.com/teleport/access-plugin-email", + "default": "146628656107.dkr.ecr.us-west-2.amazonaws.com/gravitational/teleport-plugin-email", "examples": [ - "146628656107.dkr.ecr.us-west-2.amazonaws.com/teleport/access-plugin-email" + "146628656107.dkr.ecr.us-west-2.amazonaws.com/gravitational/teleport-plugin-email" ] }, "pullPolicy": { diff --git a/charts/access/email/values.yaml b/charts/access/email/values.yaml index 51886f2bc..17e584235 100644 --- a/charts/access/email/values.yaml +++ b/charts/access/email/values.yaml @@ -5,7 +5,7 @@ # replicaCount: 1 image: - repository: 146628656107.dkr.ecr.us-west-2.amazonaws.com/teleport/access-plugin-email + repository: 146628656107.dkr.ecr.us-west-2.amazonaws.com/gravitational/teleport-plugin-email pullPolicy: IfNotPresent # Overrides the image tag whose default is the chart appVersion. tag: "" diff --git a/event-handler/Makefile b/event-handler/Makefile index 56ad0c3f7..2bad5b21c 100644 --- a/event-handler/Makefile +++ b/event-handler/Makefile @@ -22,8 +22,8 @@ KEYLEN = 1024 CLOUD_ADDR=evilmartians.teleport.sh:443 IDENTITY_FILE=example/keys/identity -DOCKER_NAME=event-handler-plugin -DOCKER_IMAGE = 146628656107.dkr.ecr.us-west-2.amazonaws.com/teleport/$(DOCKER_NAME):$(VERSION) +DOCKER_NAME=teleport-plugin-event-handler +DOCKER_IMAGE = 146628656107.dkr.ecr.us-west-2.amazonaws.com/gravitational/$(DOCKER_NAME):$(VERSION) DOCKER_IMAGE_QUAY = quay.io/gravitational/$(DOCKER_NAME):$(VERSION) DOCKER_BUILD_ARGS = --build-arg GO_VERSION=${GO_VERSION} --build-arg GITREF=$(GITREF) diff --git a/event-handler/README.md b/event-handler/README.md index 12c3454b8..d95c6e353 100644 --- a/event-handler/README.md +++ b/event-handler/README.md @@ -36,15 +36,15 @@ $ ./install ### Docker Image ```bash -$ docker pull quay.io/gravitational/event-handler-plugin:9.0.2 +$ docker pull quay.io/gravitational/teleport-plugin-event-handler:9.0.2 ``` ```bash -$ docker run quay.io/gravitational/event-handler-plugin:9.0.2 version +$ docker run quay.io/gravitational/teleport-plugin-event-handler:9.0.2 version Teleport event handler v9.0.2 git:teleport-event-handler-v9.0.2-0-g9e149895 go1.17.8 ``` -For a list of available tags, visit [https://quay.io/](https://quay.io/repository/gravitational/event-handler-plugin?tab=tags) +For a list of available tags, visit [https://quay.io/](https://quay.io/gravitational/teleport-plugin-event-handler?tab=tags) ### Building from source @@ -219,7 +219,7 @@ $ teleport-event-handler start --config teleport-event-handler.toml --start-time or with docker: ```sh -$ docker run -v :/etc/teleport-event-handler quay.io/gravitational/event-handler-plugin:9.0.2 start --config /etc/teleport-event-handler/teleport-event-handler.toml --start-time 2021-01-01T00:00:00Z +$ docker run -v :/etc/teleport-event-handler quay.io/gravitational/teleport-plugin-event-handler:9.0.2 start --config /etc/teleport-event-handler/teleport-event-handler.toml --start-time 2021-01-01T00:00:00Z ``` Note that here we used start time at the beginning of year 2021. Supposedly you have some events at the Teleport instance you are connecting to. Otherwise, you can omit `--start-time` flag, start the service and generate an events using `tctl create -f teleport-event-handler.yaml` then from the first step. `teleport-event-handler` will wait for that new events to appear and will send them to the fluentd. From 3f7e545ce332019a1709bb0880ff6d22eed6d10d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Andr=C3=A9=20Dinis?= Date: Thu, 14 Apr 2022 08:04:32 +0100 Subject: [PATCH 11/18] Disable CGO for terraform plugin (#491) We don't have a hard requirement to have CGO enabled for the terraform plugin Disabling it will generate a static binary, wich doesn't need glibc. This will the usage of the plugin without depending on glibc As an example: alpine and nixos distros Fixes #447 --- terraform/Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/terraform/Makefile b/terraform/Makefile index 503ea5d79..2930639da 100644 --- a/terraform/Makefile +++ b/terraform/Makefile @@ -8,7 +8,7 @@ TFDIR ?= example ADDFLAGS ?= BUILDFLAGS ?= $(ADDFLAGS) -ldflags '-w -s' -CGOFLAG ?= CGO_ENABLED=1 +CGOFLAG ?= CGO_ENABLED=0 RELEASE = terraform-provider-teleport-v$(VERSION)-$(OS)-$(ARCH)-bin @@ -97,4 +97,4 @@ reapply: .PHONY: destroy destroy: - terraform -chdir=$(TFDIR) destroy -auto-approve \ No newline at end of file + terraform -chdir=$(TFDIR) destroy -auto-approve From 6a100021ddbbc58365391841dfdd98b4acbbd946 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Andr=C3=A9=20Dinis?= Date: Thu, 14 Apr 2022 10:36:42 +0100 Subject: [PATCH 12/18] Add role_to_recipients to access email plugin (#488) * Add role_to_recipients to access email plugin This PR adds the role_to_recipients capability to the access email plugin. This feature allows the plugin to send emails to different recipients based on the specific roles. You can now use the following configuration **Usage** ```toml # other settings [delivery] sender = "noreply@example.com" # recipients is deprecated # cannot be used at the same time as `role_to_recipients` [role_to_recipients] "dev" = "devs-slack-channel" # either an email or list of emails "*" = ["admin@email.com"] # default list of recipients if not matched above ``` Relevant PR with the same functionality but for the slack plugin https://github.com/gravitational/teleport-plugins/pull/433 Fixes https://github.com/gravitational/teleport-plugins/issues/465 --- access/Dockerfile | 1 + access/config/recipients_map.go | 81 ++++++++++++++ access/config/recipients_map_test.go | 148 ++++++++++++++++++++++++++ access/email/app.go | 19 ++-- access/email/config.go | 67 ++++++++---- access/email/config_test.go | 152 +++++++++++++++++++++++++++ access/email/email_test.go | 20 +++- access/slack/app.go | 32 ++---- access/slack/config.go | 37 +------ access/slack/config_test.go | 7 +- access/slack/slack_test.go | 3 +- 11 files changed, 476 insertions(+), 91 deletions(-) create mode 100644 access/config/recipients_map.go create mode 100644 access/config/recipients_map_test.go create mode 100644 access/email/config_test.go diff --git a/access/Dockerfile b/access/Dockerfile index 982c7b9a9..279bf8c79 100644 --- a/access/Dockerfile +++ b/access/Dockerfile @@ -16,6 +16,7 @@ RUN --mount=type=cache,target=/go/pkg/mod go mod download # Copy the go source COPY access/${ACCESS_PLUGIN} access/${ACCESS_PLUGIN} +COPY access/config access/config COPY lib lib # Build diff --git a/access/config/recipients_map.go b/access/config/recipients_map.go new file mode 100644 index 000000000..197ea7644 --- /dev/null +++ b/access/config/recipients_map.go @@ -0,0 +1,81 @@ +/* +Copyright 2022 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 config + +import ( + "fmt" + + "github.com/gravitational/teleport-plugins/lib/stringset" + "github.com/gravitational/teleport/api/types" +) + +// RecipientsMap is a mapping of roles to recipient(s). +type RecipientsMap map[string][]string + +// UnmarshalTOML will convert the input into map[string][]string +// The input can be one of the following: +// "key" = "value" +// "key" = ["multiple", "values"] +func (r *RecipientsMap) UnmarshalTOML(in interface{}) error { + *r = make(RecipientsMap) + + recipientsMap, ok := in.(map[string]interface{}) + if !ok { + return fmt.Errorf("unexpected type for recipients %T", in) + } + + for k, v := range recipientsMap { + switch val := v.(type) { + case string: + (*r)[k] = []string{val} + case []interface{}: + for _, str := range val { + str, ok := str.(string) + if !ok { + return fmt.Errorf("unexpected type for recipients value %T", v) + } + (*r)[k] = append((*r)[k], str) + } + default: + return fmt.Errorf("unexpected type for recipients value %T", v) + } + } + + return nil +} + +// GetRecipientsFor will return the set of recipients given a list of roles and suggested reviewers. +// We create a unique list based on: +// - the list of suggestedReviewers +// - for each role, the list of reviewers +// - if the role doesn't exist in the map (or it's empty), we add the list of recipients for the default role ("*") instead +func (r RecipientsMap) GetRecipientsFor(roles, suggestedReviewers []string) []string { + recipients := stringset.New() + + for _, role := range roles { + roleRecipients := r[role] + if len(roleRecipients) == 0 { + roleRecipients = r[types.Wildcard] + } + + recipients.Add(roleRecipients...) + } + + recipients.Add(suggestedReviewers...) + + return recipients.ToSlice() +} diff --git a/access/config/recipients_map_test.go b/access/config/recipients_map_test.go new file mode 100644 index 000000000..28f196c75 --- /dev/null +++ b/access/config/recipients_map_test.go @@ -0,0 +1,148 @@ +/* +Copyright 2022 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 config + +import ( + "testing" + + "github.com/gravitational/teleport/api/types" + "github.com/pelletier/go-toml" + "github.com/stretchr/testify/require" +) + +type wrapRecipientsMap struct { + RecipientsMap RecipientsMap `toml:"role_to_recipients"` +} + +func TestRecipientsMap(t *testing.T) { + testCases := []struct { + desc string + in string + expectRecipients RecipientsMap + }{ + { + desc: "test role_to_recipients multiple format", + in: ` + [role_to_recipients] + "dev" = ["dev-channel", "admin-channel"] + "*" = "admin-channel" + `, + expectRecipients: RecipientsMap{ + "dev": []string{"dev-channel", "admin-channel"}, + types.Wildcard: []string{"admin-channel"}, + }, + }, + { + desc: "test role_to_recipients role to list of recipients", + in: ` + [role_to_recipients] + "dev" = ["dev-channel", "admin-channel"] + "prod" = ["sre-channel", "oncall-channel"] + `, + expectRecipients: RecipientsMap{ + "dev": []string{"dev-channel", "admin-channel"}, + "prod": []string{"sre-channel", "oncall-channel"}, + }, + }, + { + desc: "test role_to_recipients role to string recipient", + in: ` + [role_to_recipients] + "single" = "admin-channel" + `, + expectRecipients: RecipientsMap{ + "single": []string{"admin-channel"}, + }, + }, + { + desc: "test role_to_recipients multiple format", + in: ` + [role_to_recipients] + "dev" = ["dev-channel", "admin-channel"] + "*" = "admin-channel" + `, + expectRecipients: RecipientsMap{ + "dev": []string{"dev-channel", "admin-channel"}, + types.Wildcard: []string{"admin-channel"}, + }, + }, + { + desc: "test role_to_recipients no mapping", + in: ` + [role_to_recipients] + `, + expectRecipients: RecipientsMap{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + w := wrapRecipientsMap{} + err := toml.Unmarshal([]byte(tc.in), &w) + require.NoError(t, err) + + require.Equal(t, tc.expectRecipients, w.RecipientsMap) + }) + } +} + +func TestRecipientsMapGetRecipients(t *testing.T) { + testCases := []struct { + desc string + m RecipientsMap + roles []string + suggestedReviewers []string + output []string + }{ + { + desc: "test match exact role", + m: RecipientsMap{ + "dev": []string{"chanDev"}, + "*": []string{"chanA", "chanB"}, + }, + roles: []string{"dev"}, + suggestedReviewers: []string{}, + output: []string{"chanDev"}, + }, + { + desc: "test only default recipient", + m: RecipientsMap{ + "*": []string{"chanA", "chanB"}, + }, + roles: []string{"dev"}, + suggestedReviewers: []string{}, + output: []string{"chanA", "chanB"}, + }, + { + desc: "test deduplicate recipients", + m: RecipientsMap{ + "dev": []string{"chanA", "chanB"}, + "*": []string{"chanC"}, + }, + roles: []string{"dev"}, + suggestedReviewers: []string{"chanA", "chanB"}, + output: []string{"chanA", "chanB"}, + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + recipients := tc.m.GetRecipientsFor(tc.roles, tc.suggestedReviewers) + require.ElementsMatch(t, recipients, tc.output) + }) + } +} diff --git a/access/email/app.go b/access/email/app.go index 58a1fe374..8c56fff2f 100644 --- a/access/email/app.go +++ b/access/email/app.go @@ -22,7 +22,6 @@ import ( "github.com/gravitational/teleport-plugins/lib" "github.com/gravitational/teleport-plugins/lib/logger" - "github.com/gravitational/teleport-plugins/lib/stringset" "github.com/gravitational/teleport-plugins/lib/watcherjob" "github.com/gravitational/teleport/api/client" "github.com/gravitational/teleport/api/client/proto" @@ -242,7 +241,7 @@ func (a *App) onPendingRequest(ctx context.Context, req types.AccessRequest) err } if isNew { - if recipients := a.getEmailRecipients(ctx, req.GetSuggestedReviewers()); len(recipients) > 0 { + if recipients := a.getEmailRecipients(ctx, req.GetRoles(), req.GetSuggestedReviewers()); len(recipients) > 0 { if err := a.sendNewThreads(ctx, recipients, reqID, reqData); err != nil { return trace.Wrap(err) } @@ -293,22 +292,22 @@ func (a *App) onDeletedRequest(ctx context.Context, reqID string) error { } // getEmailRecipients converts suggested reviewers to email recipients -func (a *App) getEmailRecipients(ctx context.Context, suggestedReviewers []string) []string { +func (a *App) getEmailRecipients(ctx context.Context, roles, suggestedReviewers []string) []string { log := logger.Get(ctx) - recipients := stringset.NewWithCap(len(suggestedReviewers) + len(a.conf.Delivery.Recipients)) + validEmailRecipients := []string{} - recipients.Add(a.conf.Delivery.Recipients...) + recipients := a.conf.RoleToRecipients.GetRecipientsFor(roles, suggestedReviewers) - for _, reviewer := range suggestedReviewers { - if !lib.IsEmail(reviewer) { - log.Warningf("Failed to notify a suggested reviewer: %q does not look like a valid email", reviewer) + for _, recipient := range recipients { + if !lib.IsEmail(recipient) { + log.Warningf("Failed to notify a reviewer: %q does not look like a valid email", recipient) continue } - recipients.Add(reviewer) + validEmailRecipients = append(validEmailRecipients, recipient) } - return recipients.ToSlice() + return validEmailRecipients } // broadcastNewThreads sends notifications on a new request diff --git a/access/email/config.go b/access/email/config.go index 1bfc67a2d..4ae276eaa 100644 --- a/access/email/config.go +++ b/access/email/config.go @@ -19,8 +19,10 @@ package main import ( _ "embed" + "github.com/gravitational/teleport-plugins/access/config" "github.com/gravitational/teleport-plugins/lib" "github.com/gravitational/teleport-plugins/lib/logger" + "github.com/gravitational/teleport/api/types" "github.com/gravitational/trace" "github.com/pelletier/go-toml" ) @@ -50,11 +52,12 @@ type SMTPConfig struct { // Config stores the full configuration for the teleport-email plugin to run. type Config struct { - Teleport lib.TeleportConfig `toml:"teleport"` - Mailgun *MailgunConfig `toml:"mailgun"` - SMTP *SMTPConfig `toml:"smtp"` - Delivery DeliveryConfig `toml:"delivery"` - Log logger.Config `toml:"log"` + Teleport lib.TeleportConfig `toml:"teleport"` + Mailgun *MailgunConfig `toml:"mailgun"` + SMTP *SMTPConfig `toml:"smtp"` + Delivery DeliveryConfig `toml:"delivery"` + RoleToRecipients config.RecipientsMap `toml:"role_to_recipients"` + Log logger.Config `toml:"log"` } // TODO: Replace auth_server with addr once it is merged @@ -84,8 +87,11 @@ password = "" # password_file = "/var/lib/teleport/plugins/email/smtp_password" [delivery] -sender = "noreply@example.com" # From: email address -recipients = ["person@gmail.com"] # These recipients will receive all review requests +sender = "noreply@example.com" # From: email address + +[role_to_recipients] +"dev" = "dev-manager@example.com" # All requests to 'dev' role will be sent to this address +"*" = ["root@example.com", "admin@example.com"] # These recipients will receive review requests not handled by the roles above [log] output = "stderr" # Logger output. Could be "stdout", "stderr" or "/var/lib/teleport/email.log" @@ -115,7 +121,7 @@ func (c *MailgunConfig) CheckAndSetDefaults() error { if c.PrivateKey == "" { if c.PrivateKeyFile == "" { - return trace.BadParameter("Please, specify mailgun.private_key or mailgun.private_key_file!") + return trace.BadParameter("specify mailgun.private_key or mailgun.private_key_file") } c.PrivateKey, err = lib.ReadPassword(c.PrivateKeyFile) @@ -124,14 +130,14 @@ func (c *MailgunConfig) CheckAndSetDefaults() error { } if c.PrivateKey == "" { - return trace.BadParameter("Please, provide mailgun.private_key or mailgun.private_key_file to use Mailgun!"+ - "Ensure that password file %v is not empty!", c.PrivateKeyFile) + return trace.BadParameter("provide mailgun.private_key or mailgun.private_key_file to use Mailgun"+ + " and ensure that password file %v is not empty", c.PrivateKeyFile) } } if c.Domain == "" { - return trace.BadParameter("Please, provide mailgun.domain to use Mailgun") + return trace.BadParameter("provide mailgun.domain to use Mailgun") } return nil @@ -142,7 +148,7 @@ func (c *SMTPConfig) CheckAndSetDefaults() error { var err error if c.Host == "" { - return trace.BadParameter("Please, provide smtp.host to use SMTP") + return trace.BadParameter("provide smtp.host to use SMTP") } if c.Port == 0 { @@ -150,12 +156,12 @@ func (c *SMTPConfig) CheckAndSetDefaults() error { } if c.Username == "" { - return trace.BadParameter("Please, provide smtp.username to use SMTP") + return trace.BadParameter("provide smtp.username to use SMTP") } if c.Password == "" { if c.PasswordFile == "" { - return trace.BadParameter("Please, specify smtp.password or smtp.password_file!") + return trace.BadParameter("specify smtp.password or smtp.password_file") } c.Password, err = lib.ReadPassword(c.PasswordFile) @@ -164,8 +170,8 @@ func (c *SMTPConfig) CheckAndSetDefaults() error { } if c.Password == "" { - return trace.BadParameter("Please, provide smtp.password or smtp.password_file!"+ - "Ensure that password file %v is not empty!", c.PasswordFile) + return trace.BadParameter("provide smtp.password or smtp.password_file"+ + " and ensure that password file %v is not empty", c.PasswordFile) } } @@ -183,16 +189,35 @@ func (c *Config) CheckAndSetDefaults() error { c.Log.Severity = "info" } - // Validate emails in user aliases - for _, e := range c.Delivery.Recipients { - if !lib.IsEmail(e) { - return trace.BadParameter("Invalid email address %v in users.recipients", e) + if len(c.Delivery.Recipients) > 0 { + if len(c.RoleToRecipients) > 0 { + return trace.BadParameter("provide either delivery.recipients or role_to_recipients, not both") + } + + c.RoleToRecipients = config.RecipientsMap{ + types.Wildcard: c.Delivery.Recipients, + } + c.Delivery.Recipients = nil + } + + if len(c.RoleToRecipients) == 0 { + return trace.BadParameter("missing required value role_to_recipients") + } + if len(c.RoleToRecipients[types.Wildcard]) == 0 { + return trace.BadParameter("missing required value role_to_recipients[%v]", types.Wildcard) + } + + for role, recipientsList := range c.RoleToRecipients { + for _, recipient := range recipientsList { + if !lib.IsEmail(recipient) { + return trace.BadParameter("invalid email address %v in role_to_recipients.%s", recipient, role) + } } } // Validate mailer settings if c.SMTP == nil && c.Mailgun == nil { - return trace.BadParameter("Provide either [mailgun] or [smtp] sections to work with plugin") + return trace.BadParameter("provide either [mailgun] or [smtp] sections to work with plugin") } // Validate Mailgun settings diff --git a/access/email/config_test.go b/access/email/config_test.go new file mode 100644 index 000000000..8dc22b46f --- /dev/null +++ b/access/email/config_test.go @@ -0,0 +1,152 @@ +/* +Copyright 2022 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 ( + "os" + "path/filepath" + "testing" + + "github.com/gravitational/teleport-plugins/access/config" + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/trace" + "github.com/stretchr/testify/require" +) + +func TestRecipients(t *testing.T) { + testCases := []struct { + desc string + in string + expectErr require.ErrorAssertionFunc + expectRecipients config.RecipientsMap + }{ + { + desc: "test delivery recipients", + in: ` + [mailgun] + domain = "x" + private_key = "y" + [delivery] + sender = "email@example.org" + recipients = ["email1@example.org","email2@example.org"] + `, + expectRecipients: config.RecipientsMap{ + types.Wildcard: []string{"email1@example.org", "email2@example.org"}, + }, + }, + { + desc: "test role_to_recipients", + in: ` + [mailgun] + domain = "x" + private_key = "y" + [delivery] + sender = "email@example.org" + + [role_to_recipients] + "dev" = ["dev@example.org","sre@example.org"] + "*" = "admin@example.org" + `, + expectRecipients: config.RecipientsMap{ + "dev": []string{"dev@example.org", "sre@example.org"}, + types.Wildcard: []string{"admin@example.org"}, + }, + }, + { + desc: "test role_to_recipients but no wildcard", + in: ` + [mailgun] + domain = "x" + private_key = "y" + [delivery] + sender = "email@example.org" + + [role_to_recipients] + "dev" = ["dev@example.org","sre@example.org"] + `, + expectErr: func(tt require.TestingT, e error, i ...interface{}) { + require.Error(t, e) + require.True(t, trace.IsBadParameter(e)) + }, + }, + { + desc: "test role_to_recipients with wildcard but empty list of recipients", + in: ` + [mailgun] + domain = "x" + private_key = "y" + [delivery] + sender = "email@example.org" + + [role_to_recipients] + "dev" = "email@example.org" + "*" = [] + `, + expectErr: func(tt require.TestingT, e error, i ...interface{}) { + require.Error(t, e) + require.True(t, trace.IsBadParameter(e)) + }, + }, + { + desc: "test no recipients or role_to_recipients", + in: ` + [mailgun] + domain = "x" + private_key = "y" + [delivery] + sender = "email@example.org" + `, + expectErr: func(tt require.TestingT, e error, i ...interface{}) { + require.Error(t, e) + require.True(t, trace.IsBadParameter(e)) + }, + }, + { + desc: "test recipients and role_to_recipients", + in: ` + [slack] + token = "token" + recipients = ["dev@example.org","admin@example.org"] + + [role_to_recipients] + "dev" = ["dev@example.org","admin@example.org"] + "*" = "admin@example.org" + `, + expectErr: func(tt require.TestingT, e error, i ...interface{}) { + require.Error(t, e) + require.True(t, trace.IsBadParameter(e)) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + filePath := filepath.Join(t.TempDir(), "config_test.toml") + err := os.WriteFile(filePath, []byte(tc.in), 0777) + require.NoError(t, err) + + c, err := LoadConfig(filePath) + if tc.expectErr != nil { + tc.expectErr(t, err) + return + } + + require.NoError(t, err) + require.Equal(t, tc.expectRecipients, c.RoleToRecipients) + }) + } +} diff --git a/access/email/email_test.go b/access/email/email_test.go index 116d79d5d..6de144033 100644 --- a/access/email/email_test.go +++ b/access/email/email_test.go @@ -1,3 +1,19 @@ +/* +Copyright 2022 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 ( @@ -199,7 +215,9 @@ func (s *EmailSuite) SetupTest() { APIBase: s.mockMailgun.GetURL(), } conf.Delivery.Sender = sender - conf.Delivery.Recipients = []string{allRecipient} + conf.RoleToRecipients = map[string][]string{ + types.Wildcard: {allRecipient}, + } s.appConfig = conf s.SetContextTimeout(5 * time.Minute) diff --git a/access/slack/app.go b/access/slack/app.go index 318724708..9e13539ec 100644 --- a/access/slack/app.go +++ b/access/slack/app.go @@ -365,33 +365,23 @@ func (a *App) tryLookupDirectChannelByEmail(ctx context.Context, userEmail strin func (a *App) getMessageRecipients(ctx context.Context, req types.AccessRequest) []string { log := logger.Get(ctx) - var recipients []string - for _, role := range req.GetRoles() { - recipients = append(recipients, a.conf.Recipients[role]...) - } - - // If there are no recipient map entries for the requested roles, default to wildcard. - if len(recipients) == 0 { - recipients = a.conf.Recipients[types.Wildcard] - } - - channelSet := stringset.NewWithCap(len(req.GetSuggestedReviewers()) + len(recipients)) - - for _, recipient := range req.GetSuggestedReviewers() { - // We require SuggestedReviewers to contain email-like data. Anything else is not supported. - if !lib.IsEmail(recipient) { - log.Warningf("Failed to notify a suggested reviewer: %q does not look like a valid email", recipient) + // We receive a set from GetRecipientsFor but we still might end up with duplicate channel names. + // This can happen if this set contains the channel `C` and the email for channel `C`. + channelSet := stringset.New() + + validEmaislSuggReviewers := []string{} + for _, reviewer := range req.GetSuggestedReviewers() { + if !lib.IsEmail(reviewer) { + log.Warningf("Failed to notify a suggested reviewer: %q does not look like a valid email", reviewer) continue } - channel := a.tryLookupDirectChannelByEmail(ctx, recipient) - if channel != "" { - channelSet.Add(channel) - } + validEmaislSuggReviewers = append(validEmaislSuggReviewers, reviewer) } + recipients := a.conf.Recipients.GetRecipientsFor(req.GetRoles(), validEmaislSuggReviewers) for _, recipient := range recipients { - // Recipients from config file could contain either email or channel name or channel ID. It's up to user what format to use. + // Recipients could contain either email or channel name or channel ID. It's up to user what format to use. channel := recipient if lib.IsEmail(recipient) { channel = a.tryLookupDirectChannelByEmail(ctx, recipient) diff --git a/access/slack/config.go b/access/slack/config.go index 1bdb26a1c..a4604f387 100644 --- a/access/slack/config.go +++ b/access/slack/config.go @@ -1,9 +1,9 @@ package main import ( - "fmt" "strings" + "github.com/gravitational/teleport-plugins/access/config" "github.com/gravitational/teleport-plugins/lib" "github.com/gravitational/teleport-plugins/lib/logger" "github.com/gravitational/teleport/api/types" @@ -15,7 +15,7 @@ import ( type Config struct { Teleport lib.TeleportConfig Slack SlackConfig - Recipients RecipientsMap `toml:"role_to_recipients"` + Recipients config.RecipientsMap `toml:"role_to_recipients"` Log logger.Config } @@ -27,37 +27,6 @@ type SlackConfig struct { APIURL string } -// RecipientsMap is a mapping of roles to recipient(s). -type RecipientsMap map[string][]string - -func (r *RecipientsMap) UnmarshalTOML(in interface{}) error { - *r = make(RecipientsMap) - - recipientsMap, ok := in.(map[string]interface{}) - if !ok { - return fmt.Errorf("unexpected type for recipients %T", in) - } - - for k, v := range recipientsMap { - switch val := v.(type) { - case string: - (*r)[k] = []string{val} - case []interface{}: - for _, str := range val { - str, ok := str.(string) - if !ok { - return fmt.Errorf("unexpected type for recipients value %T", v) - } - (*r)[k] = append((*r)[k], str) - } - default: - return fmt.Errorf("unexpected type for recipients value %T", v) - } - } - - return nil -} - // LoadConfig reads the config file, initializes a new Config struct object, and returns it. // Optionally returns an error if the file is not readable, or if file format is invalid. func LoadConfig(filepath string) (*Config, error) { @@ -106,7 +75,7 @@ func (c *Config) CheckAndSetDefaults() error { return trace.BadParameter("provide either slack.recipients or role_to_recipients, not both.") } - c.Recipients = RecipientsMap{ + c.Recipients = config.RecipientsMap{ types.Wildcard: c.Slack.Recipients, } } diff --git a/access/slack/config_test.go b/access/slack/config_test.go index 319feb1d7..44051eeb0 100644 --- a/access/slack/config_test.go +++ b/access/slack/config_test.go @@ -5,6 +5,7 @@ import ( "path/filepath" "testing" + "github.com/gravitational/teleport-plugins/access/config" "github.com/gravitational/teleport/api/types" "github.com/gravitational/trace" "github.com/stretchr/testify/require" @@ -15,7 +16,7 @@ func TestRecipients(t *testing.T) { desc string in string expectErr require.ErrorAssertionFunc - expectRecipients RecipientsMap + expectRecipients config.RecipientsMap }{ { desc: "test recipients", @@ -24,7 +25,7 @@ func TestRecipients(t *testing.T) { token = "token" recipients = ["dev-channel","admin-channel"] `, - expectRecipients: RecipientsMap{ + expectRecipients: config.RecipientsMap{ types.Wildcard: []string{"dev-channel", "admin-channel"}, }, }, @@ -38,7 +39,7 @@ func TestRecipients(t *testing.T) { "dev" = ["dev-channel","admin-channel"] "*" = "admin-channel" `, - expectRecipients: RecipientsMap{ + expectRecipients: config.RecipientsMap{ "dev": []string{"dev-channel", "admin-channel"}, types.Wildcard: []string{"admin-channel"}, }, diff --git a/access/slack/slack_test.go b/access/slack/slack_test.go index 9b5ee023f..dc3994ba2 100644 --- a/access/slack/slack_test.go +++ b/access/slack/slack_test.go @@ -14,6 +14,7 @@ import ( "github.com/google/uuid" + "github.com/gravitational/teleport-plugins/access/config" "github.com/gravitational/teleport-plugins/lib" "github.com/gravitational/teleport-plugins/lib/logger" . "github.com/gravitational/teleport-plugins/lib/testing" @@ -302,7 +303,7 @@ func (s *SlackSuite) TestRecipientsConfig() { reviewer1 := s.fakeSlack.StoreUser(User{Profile: UserProfile{Email: s.userNames.reviewer1}}) reviewer2 := s.fakeSlack.StoreUser(User{Profile: UserProfile{Email: s.userNames.reviewer2}}) - s.appConfig.Recipients = RecipientsMap{ + s.appConfig.Recipients = config.RecipientsMap{ types.Wildcard: []string{reviewer2.Profile.Email, reviewer1.ID}, } From b4319594acf38eeebb5b56007be86498aee2bed3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Andr=C3=A9=20Dinis?= Date: Thu, 14 Apr 2022 17:25:58 +0100 Subject: [PATCH 13/18] Bump go to 1.17.9 (#492) --- .cloudbuild/ci/unit-tests-linux.yaml | 4 ++-- .drone.yml | 14 +++++++------- access/email/Makefile | 2 +- access/jira/Makefile | 4 ++-- access/mattermost/Makefile | 4 ++-- access/pagerduty/Makefile | 2 +- access/slack/Makefile | 4 ++-- docker/Makefile | 2 +- event-handler/Makefile | 2 +- lib/testing/integration/download.go | 16 ++++++++-------- 10 files changed, 27 insertions(+), 27 deletions(-) diff --git a/.cloudbuild/ci/unit-tests-linux.yaml b/.cloudbuild/ci/unit-tests-linux.yaml index 85d578ea8..95bfaad6a 100644 --- a/.cloudbuild/ci/unit-tests-linux.yaml +++ b/.cloudbuild/ci/unit-tests-linux.yaml @@ -1,7 +1,7 @@ steps: - - name: golang:1.17.5 + - name: golang:1.17.9 env: - - TELEPORT_GET_VERSION=v9.0.1 + - TELEPORT_GET_VERSION=v9.0.4 secretEnv: - TELEPORT_ENTERPRISE_LICENSE entrypoint: /bin/bash diff --git a/.drone.yml b/.drone.yml index 725aaaf35..f8870e6d2 100644 --- a/.drone.yml +++ b/.drone.yml @@ -24,11 +24,11 @@ steps: - make lint - name: Run tests - image: golang:1.17.5 + image: golang:1.17.9 environment: TELEPORT_ENTERPRISE_LICENSE: from_secret: TELEPORT_ENTERPRISE_LICENSE - TELEPORT_GET_VERSION: v9.0.1 + TELEPORT_GET_VERSION: v9.0.4 commands: - echo Testing plugins against Teleport $TELEPORT_GET_VERSION - make test @@ -103,7 +103,7 @@ workspace: steps: - name: Build artifacts - image: golang:1.17.5 + image: golang:1.17.9 commands: - make build-all @@ -177,7 +177,7 @@ workspace: steps: - name: Build artifacts - image: golang:1.17.5 + image: golang:1.17.9 commands: - mkdir -p build/ - export PLUGIN_TYPE=$(echo ${DRONE_TAG} | cut -d- -f2) @@ -274,7 +274,7 @@ workspace: steps: - name: Build artifacts - image: golang:1.17.5 + image: golang:1.17.9 commands: - mkdir -p build/ - make release/terraform @@ -356,7 +356,7 @@ workspace: steps: - name: Build artifacts - image: golang:1.17.5 + image: golang:1.17.9 commands: - mkdir -p build/ - make release/event-handler @@ -626,6 +626,6 @@ volumes: --- kind: signature -hmac: 9f4c1a36000b9b2637e790418a624a31c162e2bb248cc6718a1c28a82a8b10c4 +hmac: 4393ffb49dba5c0789d1f925c4e78f89d10e235dc86d476cb760afd524ec18ea ... diff --git a/access/email/Makefile b/access/email/Makefile index 8117a85a3..d26caac8f 100644 --- a/access/email/Makefile +++ b/access/email/Makefile @@ -1,5 +1,5 @@ VERSION=9.0.4 -GO_VERSION=1.17.8 +GO_VERSION=1.17.9 BUILDDIR ?= build BINARY = $(BUILDDIR)/teleport-email diff --git a/access/jira/Makefile b/access/jira/Makefile index 533a25db1..170939270 100644 --- a/access/jira/Makefile +++ b/access/jira/Makefile @@ -1,5 +1,5 @@ VERSION=9.0.4 -GO_VERSION=1.17.8 +GO_VERSION=1.17.9 BUILDDIR ?= build BINARY = $(BUILDDIR)/teleport-jira @@ -64,4 +64,4 @@ docker-push: docker-promote: docker pull ${DOCKER_IMAGE} && \ docker tag ${DOCKER_IMAGE} ${DOCKER_IMAGE_QUAY} && \ - docker push ${DOCKER_IMAGE_QUAY} \ No newline at end of file + docker push ${DOCKER_IMAGE_QUAY} diff --git a/access/mattermost/Makefile b/access/mattermost/Makefile index c1c7d19a4..923d58905 100644 --- a/access/mattermost/Makefile +++ b/access/mattermost/Makefile @@ -1,5 +1,5 @@ VERSION=9.0.4 -GO_VERSION=1.17.8 +GO_VERSION=1.17.9 BUILDDIR ?= build BINARY = $(BUILDDIR)/teleport-mattermost @@ -64,4 +64,4 @@ docker-push: docker-promote: docker pull ${DOCKER_IMAGE} && \ docker tag ${DOCKER_IMAGE} ${DOCKER_IMAGE_QUAY} && \ - docker push ${DOCKER_IMAGE_QUAY} \ No newline at end of file + docker push ${DOCKER_IMAGE_QUAY} diff --git a/access/pagerduty/Makefile b/access/pagerduty/Makefile index 3f5046a30..1f513c714 100644 --- a/access/pagerduty/Makefile +++ b/access/pagerduty/Makefile @@ -1,5 +1,5 @@ VERSION=9.0.4 -GO_VERSION=1.17.8 +GO_VERSION=1.17.9 BUILDDIR ?= build BINARY = $(BUILDDIR)/teleport-pagerduty diff --git a/access/slack/Makefile b/access/slack/Makefile index deec67e8f..9b6df0d52 100644 --- a/access/slack/Makefile +++ b/access/slack/Makefile @@ -1,5 +1,5 @@ VERSION=9.0.4 -GO_VERSION=1.17.8 +GO_VERSION=1.17.9 BUILDDIR ?= build BINARY = $(BUILDDIR)/teleport-slack @@ -64,4 +64,4 @@ docker-push: ## Push docker image with the plugin. docker-promote: docker pull ${DOCKER_IMAGE} && \ docker tag ${DOCKER_IMAGE} ${DOCKER_IMAGE_QUAY} && \ - docker push ${DOCKER_IMAGE_QUAY} \ No newline at end of file + docker push ${DOCKER_IMAGE_QUAY} diff --git a/docker/Makefile b/docker/Makefile index f22d1fe2d..054c9688f 100644 --- a/docker/Makefile +++ b/docker/Makefile @@ -10,7 +10,7 @@ MKFILE_PATH := $(abspath $(lastword $(MAKEFILE_LIST))) # enterprise version, but has to be available on get.gravitational.com RELEASE ?= teleport-ent-v6.0.2-linux-amd64-bin -RUNTIME ?= go1.17.5 +RUNTIME ?= go1.17.9 BBOX ?= quay.io/gravitational/teleport-buildbox:$(RUNTIME) # Teleport CLI and plugins CLI flags to pass to them on start diff --git a/event-handler/Makefile b/event-handler/Makefile index 2bad5b21c..77c52dc85 100644 --- a/event-handler/Makefile +++ b/event-handler/Makefile @@ -1,5 +1,5 @@ VERSION=9.0.4 -GO_VERSION=1.17.8 +GO_VERSION=1.17.9 OS ?= $(shell go env GOOS) ARCH ?= $(shell go env GOARCH) diff --git a/lib/testing/integration/download.go b/lib/testing/integration/download.go index c2299835c..4e39696c0 100644 --- a/lib/testing/integration/download.go +++ b/lib/testing/integration/download.go @@ -49,15 +49,15 @@ type downloadVersion struct { var downloadVersions = map[downloadVersionKey]downloadVersion{ // Teleport v9.0.1 Enterprise binaries - {"v9.0.1", "darwin", "amd64", true}: {sha256: lib.MustHexSHA256("21db6d77662f7660797cf993101504deba4498c15b6d5faf54d75021b22c1bd5")}, - {"v9.0.1", "linux", "amd64", true}: {sha256: lib.MustHexSHA256("c34e1b083f85754fd0b5e029add0df01df4ee70c3a2a4f4ba11dd4ae39606ed3")}, - {"v9.0.1", "linux", "arm64", true}: {sha256: lib.MustHexSHA256("04cb4c412f12af64320855a8333934d8644886b7eb47c756d8c74ba9e03265d4")}, - {"v9.0.1", "linux", "arm", true}: {sha256: lib.MustHexSHA256("e67ced67409c4bbb4d5ac69fcae237787d9f992ba151a79325de2351519baa67")}, + {"v9.0.4", "darwin", "amd64", true}: {sha256: lib.MustHexSHA256("a6fa9c515e2b3daf97b5649453f0ce059567d1d8f731e24b7ba24b2386026215")}, + {"v9.0.4", "linux", "amd64", true}: {sha256: lib.MustHexSHA256("804353458a99510eaf62b3de48e6737f9dec40df5abe0e2259cc32780e51d6ed")}, + {"v9.0.4", "linux", "arm64", true}: {sha256: lib.MustHexSHA256("33657413d90e7a84e1a05af646c7ca9169dfb0807a4110f37168cb2750d1c9c3")}, + {"v9.0.4", "linux", "arm", true}: {sha256: lib.MustHexSHA256("e0fa5435bc0e71d883d23f5211f1bc0dd0c821b3533b5dec7b198409bbd99914")}, // Teleport v9.0.1 OSS binaries - {"v9.0.1", "darwin", "amd64", false}: {sha256: lib.MustHexSHA256("8c65e675ffacd9707f787e359da20a697851958af21526c084f3c28307454eae")}, - {"v9.0.1", "linux", "amd64", false}: {sha256: lib.MustHexSHA256("abe36e9b75ccae8473fd35bef250238e66f5c68ef0c251ec11987b58b79d8e4b")}, - {"v9.0.1", "linux", "arm64", false}: {sha256: lib.MustHexSHA256("0d48db3f4b75d5341777bcacc182af32cfc76cab5aaeadfca3ae258099053e1c")}, - {"v9.0.1", "linux", "arm", false}: {sha256: lib.MustHexSHA256("4335d869058f6fcb5b0a0ba5f3d27708c1b3a8e5334b7917168206064b76f83c")}, + {"v9.0.4", "darwin", "amd64", false}: {sha256: lib.MustHexSHA256("2d61469e2b64dab19fe5f7471181a0f417a32f80831280c7ffdb8ea6035bd831")}, + {"v9.0.4", "linux", "amd64", false}: {sha256: lib.MustHexSHA256("76fbaec6def758b7d24fb6aae63a83f47f9a74c59fad5af4a9cf3455b7d1774c")}, + {"v9.0.4", "linux", "arm64", false}: {sha256: lib.MustHexSHA256("597108095e04751a02e3b355662f7520b8cfdc0015318ef41178026defaa3406")}, + {"v9.0.4", "linux", "arm", false}: {sha256: lib.MustHexSHA256("0e9053730862ccd62c872303fe06cfea928c5a44d4b3667119960a9e6665dc80")}, } // GetEnterprise downloads a Teleport Enterprise distribution. From f73ae41964057565d825d505de63f51d1b3d9a88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Andr=C3=A9=20Dinis?= Date: Thu, 14 Apr 2022 22:53:21 +0100 Subject: [PATCH 14/18] Fix darwin build when building darwin artifacts (#502) We have multiple errors for the `test-darwin` step on our CI It happens because we can't remove the files inside our temp directory This directory, which contains the go packages, is created and everything inside of it is readonly (this is by design) The command `go clean -modcache` was suppose to remove the entire cache, but looks like it's not working (or something else is up, not sure) This commit adds an extra step: change the every folder/file to be RW on this temporary directory This should allow us to, latter on, remove the entire directory --- .drone.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.drone.yml b/.drone.yml index f8870e6d2..c4c911286 100644 --- a/.drone.yml +++ b/.drone.yml @@ -65,6 +65,7 @@ steps: # This will remove subdirectories under pkg/mod which 0400 permissions. # See for more details: https://github.com/golang/go/issues/27455 - go clean -modcache + - chmod -R u+rw /tmp/teleport-plugins/build-darwin/go - rm -rf /tmp/teleport-plugins/test-darwin/go - mkdir -p /tmp/teleport-plugins/test-darwin/go @@ -142,6 +143,7 @@ steps: # This will remove subdirectories under pkg/mod which 0400 permissions. # See for more details: https://github.com/golang/go/issues/27455 - go clean -modcache + - chmod -R u+rw /tmp/teleport-plugins/build-darwin/go - rm -rf /tmp/teleport-plugins/build-darwin/go - mkdir -p /tmp/teleport-plugins/build-darwin/go/cache @@ -626,6 +628,6 @@ volumes: --- kind: signature -hmac: 4393ffb49dba5c0789d1f925c4e78f89d10e235dc86d476cb760afd524ec18ea +hmac: 47fcd1903112ba7c637bcf5e33d91e956d0c09da75b40698f6e0891b2ef165c4 ... From cd32f94955131f5d7918dd4467b7adbf434f8d99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Andr=C3=A9=20Dinis?= Date: Fri, 15 Apr 2022 08:17:59 +0100 Subject: [PATCH 15/18] fix typo on last commit (#503) --- .drone.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.drone.yml b/.drone.yml index c4c911286..dea11809f 100644 --- a/.drone.yml +++ b/.drone.yml @@ -65,7 +65,7 @@ steps: # This will remove subdirectories under pkg/mod which 0400 permissions. # See for more details: https://github.com/golang/go/issues/27455 - go clean -modcache - - chmod -R u+rw /tmp/teleport-plugins/build-darwin/go + - chmod -R u+rw /tmp/teleport-plugins/test-darwin/go - rm -rf /tmp/teleport-plugins/test-darwin/go - mkdir -p /tmp/teleport-plugins/test-darwin/go @@ -628,6 +628,6 @@ volumes: --- kind: signature -hmac: 47fcd1903112ba7c637bcf5e33d91e956d0c09da75b40698f6e0891b2ef165c4 +hmac: 2a080aaa88a5b3e244732c5a062231300a14b6b3d789833b3fa0a1728dfa727b ... From 6e76cc22e624b355ea331a4e9a54f00290172be7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Andr=C3=A9=20Dinis?= Date: Tue, 19 Apr 2022 08:04:42 +0100 Subject: [PATCH 16/18] bump teleport version for darwin tests (#504) --- .drone.yml | 4 ++-- lib/testing/integration/download.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.drone.yml b/.drone.yml index dea11809f..8f116190d 100644 --- a/.drone.yml +++ b/.drone.yml @@ -73,7 +73,7 @@ steps: environment: TELEPORT_ENTERPRISE_LICENSE: from_secret: TELEPORT_ENTERPRISE_LICENSE - TELEPORT_GET_VERSION: v9.0.1 + TELEPORT_GET_VERSION: v9.0.4 GOPATH: /tmp/teleport-plugins/test-darwin/go GOCACHE: /tmp/teleport-plugins/test-darwin/go/cache commands: @@ -628,6 +628,6 @@ volumes: --- kind: signature -hmac: 2a080aaa88a5b3e244732c5a062231300a14b6b3d789833b3fa0a1728dfa727b +hmac: d5a5c8557a0903baf070be22b6fb54bf1e830ba388a32cb8d46cc519015b0585 ... diff --git a/lib/testing/integration/download.go b/lib/testing/integration/download.go index 4e39696c0..2bf2c33fb 100644 --- a/lib/testing/integration/download.go +++ b/lib/testing/integration/download.go @@ -48,12 +48,12 @@ type downloadVersion struct { } var downloadVersions = map[downloadVersionKey]downloadVersion{ - // Teleport v9.0.1 Enterprise binaries + // Teleport v9.0.4 Enterprise binaries {"v9.0.4", "darwin", "amd64", true}: {sha256: lib.MustHexSHA256("a6fa9c515e2b3daf97b5649453f0ce059567d1d8f731e24b7ba24b2386026215")}, {"v9.0.4", "linux", "amd64", true}: {sha256: lib.MustHexSHA256("804353458a99510eaf62b3de48e6737f9dec40df5abe0e2259cc32780e51d6ed")}, {"v9.0.4", "linux", "arm64", true}: {sha256: lib.MustHexSHA256("33657413d90e7a84e1a05af646c7ca9169dfb0807a4110f37168cb2750d1c9c3")}, {"v9.0.4", "linux", "arm", true}: {sha256: lib.MustHexSHA256("e0fa5435bc0e71d883d23f5211f1bc0dd0c821b3533b5dec7b198409bbd99914")}, - // Teleport v9.0.1 OSS binaries + // Teleport v9.0.4 OSS binaries {"v9.0.4", "darwin", "amd64", false}: {sha256: lib.MustHexSHA256("2d61469e2b64dab19fe5f7471181a0f417a32f80831280c7ffdb8ea6035bd831")}, {"v9.0.4", "linux", "amd64", false}: {sha256: lib.MustHexSHA256("76fbaec6def758b7d24fb6aae63a83f47f9a74c59fad5af4a9cf3455b7d1774c")}, {"v9.0.4", "linux", "arm64", false}: {sha256: lib.MustHexSHA256("597108095e04751a02e3b355662f7520b8cfdc0015318ef41178026defaa3406")}, From 9df887f1c85920415115bc11d895304db0fa5086 Mon Sep 17 00:00:00 2001 From: Bence Kiglics Date: Thu, 21 Apr 2022 01:58:27 +0200 Subject: [PATCH 17/18] Bumped version to latest (#509) --- charts/access/email/Chart.yaml | 2 +- .../email/tests/__snapshot__/configmap_test.yaml.snap | 6 +++--- .../email/tests/__snapshot__/deployment_test.yaml.snap | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/charts/access/email/Chart.yaml b/charts/access/email/Chart.yaml index e31707429..64cdc5c1f 100644 --- a/charts/access/email/Chart.yaml +++ b/charts/access/email/Chart.yaml @@ -21,4 +21,4 @@ version: 1.0.0 # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "9.0.0" +appVersion: "9.0.4" diff --git a/charts/access/email/tests/__snapshot__/configmap_test.yaml.snap b/charts/access/email/tests/__snapshot__/configmap_test.yaml.snap index 3a5061bf1..f7735b8c7 100644 --- a/charts/access/email/tests/__snapshot__/configmap_test.yaml.snap +++ b/charts/access/email/tests/__snapshot__/configmap_test.yaml.snap @@ -24,7 +24,7 @@ should match the snapshot (mailgun on): app.kubernetes.io/instance: RELEASE-NAME app.kubernetes.io/managed-by: Helm app.kubernetes.io/name: teleport-plugin-email - app.kubernetes.io/version: 9.0.0 + app.kubernetes.io/version: 9.0.4 helm.sh/chart: teleport-plugin-email-1.0.0 name: RELEASE-NAME-teleport-plugin-email should match the snapshot (smtp on): @@ -56,7 +56,7 @@ should match the snapshot (smtp on): app.kubernetes.io/instance: RELEASE-NAME app.kubernetes.io/managed-by: Helm app.kubernetes.io/name: teleport-plugin-email - app.kubernetes.io/version: 9.0.0 + app.kubernetes.io/version: 9.0.4 helm.sh/chart: teleport-plugin-email-1.0.0 name: RELEASE-NAME-teleport-plugin-email should match the snapshot (smtp on, password file): @@ -88,6 +88,6 @@ should match the snapshot (smtp on, password file): app.kubernetes.io/instance: RELEASE-NAME app.kubernetes.io/managed-by: Helm app.kubernetes.io/name: teleport-plugin-email - app.kubernetes.io/version: 9.0.0 + app.kubernetes.io/version: 9.0.4 helm.sh/chart: teleport-plugin-email-1.0.0 name: RELEASE-NAME-teleport-plugin-email diff --git a/charts/access/email/tests/__snapshot__/deployment_test.yaml.snap b/charts/access/email/tests/__snapshot__/deployment_test.yaml.snap index 10e5482db..57321f315 100644 --- a/charts/access/email/tests/__snapshot__/deployment_test.yaml.snap +++ b/charts/access/email/tests/__snapshot__/deployment_test.yaml.snap @@ -7,7 +7,7 @@ should match the snapshot: app.kubernetes.io/instance: RELEASE-NAME app.kubernetes.io/managed-by: Helm app.kubernetes.io/name: teleport-plugin-email - app.kubernetes.io/version: 9.0.0 + app.kubernetes.io/version: 9.0.4 helm.sh/chart: teleport-plugin-email-1.0.0 name: RELEASE-NAME-teleport-plugin-email spec: From b43848bc7b79cfda3101209995b3d90216a6a741 Mon Sep 17 00:00:00 2001 From: Walt Date: Wed, 20 Apr 2022 19:18:14 -0700 Subject: [PATCH 18/18] Resign .drone.yml (#510) https://github.com/gravitational/teleport-plugins/pull/504 had an incorrect signature, and is causing post-merge builds to hang. --- .drone.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.drone.yml b/.drone.yml index 8f116190d..ed4982c21 100644 --- a/.drone.yml +++ b/.drone.yml @@ -628,6 +628,6 @@ volumes: --- kind: signature -hmac: d5a5c8557a0903baf070be22b6fb54bf1e830ba388a32cb8d46cc519015b0585 +hmac: 5cfe82e8c35ed3e6fbfbada4e08648e4ca3ea2af9e950ab0d2f98c6a30f00414 ...