Create Role-bindings for a GitHub Team Using Terraform

Procedural tutorial outlining how to use Terraform to create Chainguard role-bindings for members of a GitHub team.

There may be cases where an organization will want multiple users to have access to the same Chainguard organization. Chainguard allows you to grant other users access to Chainguard by generating an invite link or code.

In addition, you can now grant access to users using Terraform and identity providers like GitHub, GitLab, and Google. You can also manage access through these providers’ existing group structures, like GitHub Teams or GitLab Groups. Granting access through Terraform helps to reduce the risk of unwanted users gaining access to Chainguard.

This guide outlines one method of using Terraform to grant members of a GitHub team access to the resources managed by a Chainguard organization. It also highlights a few other Terraform configurations you can use to manage role-bindings in the Chainguard platform. Although this guide is specific to GitHub, the same approach can be used for other systems.

Prerequisites

To complete this guide, you will need the following.

Setting up your Environment

There are a few things you must have in place in order to follow this guide. First, create a testing directory to hold the Terraform configuration and navigate into it.

mkdir ~/github-team && cd $_

This will help make it easier to clean up your system at the end of this guide.

Next, you’ll need to set up a few environment variables that the Terraform configuration in this guide assumes you will have in place.

Start by creating an environment variable named GITHUB_ORG that points to the name of your GitHub organization. Run the following command to create this variable, but be sure to replace <your GitHub organization> with actual name of your organization as it appears in URLs. For example, if your organization owns a repository at the URL htttps://github.com/orgName-example/repository-name, then the value you would pass here would be orgName-example.

export GITHUB_ORG=<your GitHub organization>

Next, create a variable named GITHUB_TEAM set to the slug of the GitHub team for which you want to create a set of role-bindings. The Terraform configuration will use this detail to find and retrieve information about your GitHub team.

If you aren’t sure of what your team’s slug is, you can find it with gh, the GitHub command line interface. You can use a command like the following to retrieve a list of all your organization’s teams.

gh api -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" /orgs/$GITHUB_ORG/teams

Scroll through this command’s output to find the slug value for the team in question.

[
. . .

  {
	"name": "Team Name",
	"id": 9999999,
	"node_id": "T_kwDOBTYtm84AbTQX",
	"slug": "team-slug",
. . .
  },
. . .
]

With the team’s slug in hand, run the following command to create the GITHUB_TEAM environment variable.

export GITHUB_TEAM=<your GitHub team slug>

Following that, you will need to provide Terraform with your GitHub personal access token so it can access information related to your GitHub organization. Rather than hard coding your token into the Terraform configuration or having Terraform prompt you to enter it manually, you can create an environment variable named GITHUB_TOKEN which Terraform will automatically use. Create this variable with the following command.

export GITHUB_TOKEN=<your GitHub token>

Lastly, the Chainguard role-bindings that this guide’s Terraform configuration will create must all be tied to an organization. Create another variable named CHAINGUARD_ORG with the following command, replacing <UIDP of target Chainguard IAM organization> with the UIDP of the Chainguard IAM organization you want to tie the role-bindings to. You can find the UIDP for your Chainguard IAM organization by running chainctl iam organizations ls -o table.

export CHAINGUARD_ORG="<UIDP of target Chainguard IAM organization>"

Following that, you will have everything you need in place to set up the Terraform configuration.

Creating your Terraform Configuration

As mentioned previously, we will be using Terraform to create role-bindings for each user in a GitHub team, giving them access to resources associated with a given Chainguard organization. This guide outlines how to create two Terraform configuration files that, together, will produce a set of such role-bindings.

To help explain both files’ purposes, we will go over what they do and how to create each one individually.

main.tf

First, we will create a main.tf file which will set up the necessary Terraform providers.

This file will consist of the following lines.

terraform {
  required_providers {
    chainguard = {
      source = "chainguard-dev/chainguard"
    }
    github = {
      source = "integrations/github"
    }
  }
}

provider "github" {
  owner = "$GITHUB_ORG"
}

The terraform block defines the sources for the chainguard and github providers.

The provider block sets up the github provider with one argument — owner — that points to the GITHUB_ORG variable you set previously.

Create the main.tf file with the following command.

cat  <<EOF > main.tf
terraform {
  required_providers {
    chainguard = {
      source = "chainguard-dev/chainguard"
    }
    github = {
      source = "integrations/github"
    }
  }
}

provider "github" {
  owner = "$GITHUB_ORG"
}
EOF

Next, you will create the other configuration file that will actually create the role-bindings for your GitHub team.

rolebindings.tf

The rolebindings.tf file will contain a few separate blocks that retrieve information about the GitHub team members and create Chainguard role-bindings for each one.

The first block creates a github_team data source named team.

data "github_team" "team" {
  slug = "$GITHUB_TEAM"
}

Using the arguments you provided in the github provider block in main.tf, Terraform will search for any GitHub teams matching the slug specified within this block. If Terraform can find a team with a matching slug in the specified GitHub organization, then you will be able to pull more data about this team down from GitHub.

After the first block retrieves information about the team, we need to retrieve information about each member of the team in order to create an identity for each of them. To this end, the next block creates a github_user data source named team_members.

data "github_user" "team_members" {
  for_each = toset(data.github_team.team.members)
  username = each.key
}

Because, we want this source to represent every member of the team, this block starts with a for_each meta-argument which accepts a toset map of the GitHub team members derived from the github_team source created previously.

Additionally, the github_user data source requires you to set the username argument to whichever user you want the data source to represent. In the Terraform Language, each.key is the map key corresponding to the objects referenced in the for_each argument. In this case, it means the username variable will be tied to a mapping of each member of the GitHub team within this Terraform configuration.

The next block retrieves Chainguard identities for each member of the GitHub team.

data "chainguard_identity" "team_ids" {
  for_each = toset([for x in data.github_user.team_members : x.id])

  issuer  = "https://auth.chainguard.dev/"
  subject = "github|${each.key}"
}

This block’s for_each meta-argument iterates through each member of the team. For each iteration, it retrieves that user’s GitHub ID and then retrieves a Chainguard identity that it derives using that GitHub ID.

If there are members of the GitHub team who have not yet registered with Chainguard, this method will still assign them the correct permissions when they log in for the first time.

The next block retrieves the predefined viewer role from Chainguard.

data "chainguard_role" "viewer" {
  name = "viewer"
}

The final block puts all this information together to create the role-bindings for each member of the team.

resource "chainguard_rolebinding" "cg-binding" {
  for_each = data.chainguard_identity.team_ids
  identity = each.value.id
  group    = "$CHAINGUARD_ORG"
  role     = data.chainguard_role.viewer.items[0].id
}

This resource block iterates through the list of Chainguard identities, assigns each one to the IAM organization specified by the group argument, and binds each identity to the viewer role. Here, the group argument is set to the CHAINGUARD_ORG variable you created at the start of this guide.

Create the rolebindings.tf file with the following command.

cat  <<EOF > rolebindings.tf
data "github_team" "team" {
  slug = "$GITHUB_TEAM"
}

data "github_user" "team_members" {
  for_each = toset(data.github_team.team.members)
  username = each.key
}

data "chainguard_identity" "team_ids" {
  for_each = toset([for x in data.github_user.team_members : x.id])

  issuer  = "https://auth.chainguard.dev/"
  subject = "github|\${each.key}"
}

data "chainguard_role" "viewer" {
  name = "viewer"
}

resource "chainguard_rolebinding" "cg-binding" {
  for_each = data.chainguard_identity.team_ids
  identity = each.value.id
  group    = "$CHAINGUARD_ORG"
  role     = data.chainguard_role.viewer.items[0].id
}
EOF

Note that the fourteenth line of this file contains a backslash (\).

  subject = "github|\${each.key}"

This is an escape character which will prevent the dollar sign in that line from causing a bad substitution error.

Now that your Terraform configuration is in place, you’re ready to apply it and create role-bindings for each member of your GitHub team.

Applying your Terraform Configuration

First, run terraform init to initialize Terraform’s working directory.

terraform init

Then run terraform plan. This will produce a speculative execution plan that outlines what steps Terraform will take to create the resources defined in the files you set up in the last section.

terraform plan

If the plan worked successfully and you’re satisfied that it will produce the resources you expect, you can apply it.

terraform apply

Before going through with applying the Terraform configuration, this command will prompt you to confirm that you want it to do so. Enter yes to apply the configuration.

. . .

Plan: 2 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value:

After pressing ENTER, the command will complete.

. . .

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

Following this, any members of your GitHub team for whom you’ve created role-bindings will be able to view the resources associated with the Chainguard organization you specified. To do so, they need to log in to the Chainguard platform, either by logging into the Chainguard Console or with the following command.

chainctl auth login

After navigating to the Console or running the login command, they will be presented with the following login flow.

Screenshot of the default Chainguard login flow. It includes the Linky logo above the words &ldquo;Welcome. Log in to Chainguard to continue to Chainguard.&rdquo; Below this are three buttons, one reading &ldquo;Continue with Google&rdquo;, one reading &ldquo;Continue with GitHUb&rdquo;, and a third reading &ldquo;Continue with GitLab&rdquo;.

There, they must click the Continue with GitHub button to continue logging in under their GitHub account. Chainguard will immediately recognize their GitHub account because it is tied to the role-binding you created in the previous step, and they will be able to view the resources associated with the Chainguard organization specified in your Terraform configuration.

Optional Configurations

The Terraform configuration used in this guide is meant to serve as a starting point, and we encourage you to tweak and expand on it to suit your organization’s needs. This section contains a few alternative configurations that you may find useful.

For example, rather than applying the viewer role to your team’s role-bindings, you can apply one of the other built-in roles, or any custom roles created within your Chainguard organization. The following data and resource block examples could be used in place of the ones used in this guide’s rolebindings.tf file. Instead of granting the identities the viewer role, this grants them the editor role.

data "chainguard_roles" "editor" {
  name = "editor"
}

resource "chainguard_rolebinding" "cg-binding" {
  for_each = toset(data.chainguard_identity.team_ids)
  identity = each.value.id
  group	= "$CHAINGUARD_ORG"
  role     = data.chainguard_roles.editor.items[0].id
}

The Terraform configuration language is quite flexible. You can update your Terraform configuration to retrieve information about a single user, rather than an entire team.

data "github_user" "user" {
  username = "$USERNAME"
}

Likewise, you can retrieve a GitHub user’s Chainguard identity without having to include the for_each meta-argument.

data "chainguard_identity" "gh-user-chainguard-rb" {
  issuer  = "https://auth.chainguard.dev/"
  subject = "github|${data.github_user.$USERNAME.id}"
}

You can refer to the Terraform language documentation for more information on extending the configuration outlined in this guide to suit your own needs.

Removing Sample Resources

To remove the resources Terraform created, you can run the terraform destroy command.

terraform destroy

This will destroy the Chainguard role-bindings created for your GitHub team. You can then remove the working directory to clean up your system.

rm -r ~/github-team/

Following that, all of the example resources created in this guide will be removed from your system.

Learn More

The procedure outlined in this tutorial can be tweaked to work with other identity providers, including Google and GitLab.

Last updated: 2024-05-09 08:48