Add tctl terraform env scope support (#68739)

This commit is contained in:
Hugo Shaka
2026-07-22 19:16:04 +00:00
committed by GitHub
parent 8a2fe6a94c
commit ba0bd0c96a
5 changed files with 418 additions and 37 deletions
+1
View File
@@ -2839,6 +2839,7 @@ Flags:
|`--bot-ttl`|`1h`|Time-to-live of the Bot resource. The bot will be removed after this period. Defaults to \[1h\]|
|`--resource-prefix`|`tctl-terraform-env-`|Resource prefix to use when creating the Terraform role and bots. Defaults to \[tctl-terraform-env-\]|
|`--role`|*no default*|Role used by Terraform. The role must already exist in Teleport. When not specified, uses the default role "terraform-provider"|
|`--scope`|*no default*|Scope of the bot used by the Terraform provider. When empty, TF provider runs unscoped.|
## tctl tokens add
+169
View File
@@ -35,16 +35,24 @@ import (
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
kyaml "k8s.io/apimachinery/pkg/util/yaml"
"github.com/gravitational/teleport/api/breaker"
"github.com/gravitational/teleport/api/client"
"github.com/gravitational/teleport/api/client/webclient"
"github.com/gravitational/teleport/api/constants"
scopedaccessv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/access/v1"
scopesv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/v1"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/utils/wait"
"github.com/gravitational/teleport/integration/helpers"
"github.com/gravitational/teleport/lib/auth/authclient"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/reversetunnelclient"
"github.com/gravitational/teleport/lib/scopes"
scopedaccess "github.com/gravitational/teleport/lib/scopes/access"
"github.com/gravitational/teleport/lib/service/servicecfg"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/teleport/lib/utils/log/logtest"
"github.com/gravitational/teleport/tool/tctl/common"
@@ -200,6 +208,141 @@ func TestTCTLTerraformCommand_AuthJoin(t *testing.T) {
connectWithCredentialsFromVars(t, vars, clt)
}
// TestTCTLTerraformCommand_ScopedProxyJoin validates that the command `tctl terraform env`
// can run from an unscoped admin against a Teleport Proxy service and generates valid
// scoped credentials Terraform can use to connect to Teleport.
func TestTCTLTerraformCommand_ScopedProxyJoin(t *testing.T) {
// test is not Parallel because of the metrics black hole
testDir := t.TempDir()
prometheus.DefaultRegisterer = metricRegistryBlackHole{}
// Test setup: creating a teleport instance running auth and proxy
clusterName := "root.example.com"
cfg := helpers.InstanceConfig{
ClusterName: clusterName,
HostID: uuid.New().String(),
NodeName: helpers.Loopback,
Logger: logtest.NewLogger(),
}
cfg.Listeners = helpers.SingleProxyPortSetup(t, &cfg.Fds)
rc := helpers.NewInstance(t, cfg)
rcConf := servicecfg.MakeDefaultConfig()
rcConf.DataDir = filepath.Join(testDir, "data")
rcConf.Auth.Enabled = true
rcConf.Proxy.Enabled = true
rcConf.SSH.Enabled = false
rcConf.Proxy.DisableWebInterface = true
rcConf.Version = "v3"
rcConf.Auth.NetworkingConfig.SetProxyListenerMode(types.ProxyListenerMode_Multiplex)
rcConf.ScopesFeatures = scopes.Features{Enabled: true}
testUsername := "test-user"
createTCTLTerraformCrossScopeAdmin(t, testUsername, rc)
// Test setup: starting the Teleport instance
err := rc.CreateEx(t, nil, rcConf)
require.NoError(t, err)
err = rc.Start()
require.NoError(t, err)
t.Cleanup(func() {
assert.NoError(t, rc.StopAll())
})
// Test setup: obtaining and authclient connected via the proxy
clt := getAuthClientForProxy(t, rc, testUsername, time.Hour)
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
_, err = clt.Ping(ctx)
require.NoError(t, err)
createDefaultTerraformScopedRole(t, ctx, clt)
addr, err := rc.Process.ProxyWebAddr()
require.NoError(t, err)
// Test execution, running the tctl command
tctlCfg := &servicecfg.Config{}
err = tctlCfg.SetAuthServerAddresses([]utils.NetAddr{*addr})
require.NoError(t, err)
tctlCommand := common.TerraformCommand{}
const testScope = "/test"
app := kingpin.New("test", "test")
tctlCommand.Initialize(app, nil, tctlCfg)
_, err = app.Parse([]string{"terraform", "env", "--scope", testScope})
require.NoError(t, err)
// Create io buffer writer
stdout := &bytes.Buffer{}
err = tctlCommand.RunEnvCommand(ctx, clt, stdout, os.Stderr)
require.NoError(t, err)
vars := parseExportedEnvVars(t, stdout)
require.Contains(t, vars, constants.EnvVarTerraformAddress)
require.Contains(t, vars, constants.EnvVarTerraformIdentityFileBase64)
// Test validation: connect with the credentials in env vars and do a ping
require.Equal(t, addr.String(), vars[constants.EnvVarTerraformAddress])
connectWithCredentialsFromVars(t, vars, clt)
// Test validation: make sure no role assignment is left. We don't know the assignment's name so we have to list them all initially.
resp, err := clt.ScopedAccessServiceClient().ListScopedRoleAssignments(ctx, scopedaccessv1.ListScopedRoleAssignmentsRequest_builder{
ScopeFilter: scopesv1.Filter_builder{
Scope: testScope,
Mode: scopesv1.Mode_MODE_EXACT,
}.Build(),
}.Build())
require.NoError(t, err)
switch len(resp.GetAssignments()) {
case 0:
// assignment already deleted
return
case 1:
// Assignment still exists, this might be a race against the cache.
// Will retry a few times.
_, err = wait.UntilNotFound(ctx, func(ctx context.Context) (*scopedaccessv1.GetScopedRoleAssignmentResponse, error) {
return clt.ScopedAccessServiceClient().GetScopedRoleAssignment(ctx, scopedaccessv1.GetScopedRoleAssignmentRequest_builder{
Name: resp.GetAssignments()[0].GetMetadata().GetName(),
SubKind: resp.GetAssignments()[0].GetSubKind(),
Scope: resp.GetAssignments()[0].GetScope(),
}.Build())
})
require.NoError(t, err)
default:
require.Fail(t, "unexpected number of role assignments: %v", len(resp.GetAssignments()))
}
}
func createDefaultTerraformScopedRole(t *testing.T, ctx context.Context, clt *authclient.Client) {
reader, err := utils.OpenFileNoUnsafeLinks("../integrations/terraform/examples/provider/scoped-provider-role.yaml")
require.NoError(t, err)
decoder := kyaml.NewYAMLOrJSONDecoder(reader, defaults.LookaheadBufSize)
var raw services.UnknownResource
err = decoder.Decode(&raw)
require.NoError(t, err)
r, err := services.UnmarshalProtoResource[*scopedaccessv1.ScopedRole](raw.Raw, services.DisallowUnknown())
require.NoError(t, err)
_, err = clt.ScopedAccessServiceClient().CreateScopedRole(ctx, scopedaccessv1.CreateScopedRoleRequest_builder{
Role: r,
}.Build())
require.NoError(t, err)
// Wait for the role to enter the cache, there is a race. If the test wins, tctl will fail to find the role.
_, err = wait.UntilFound(ctx, func(ctx context.Context) (*scopedaccessv1.GetScopedRoleResponse, error) {
return clt.ScopedAccessServiceClient().GetScopedRole(ctx,
scopedaccessv1.GetScopedRoleRequest_builder{
Name: r.GetMetadata().GetName(),
Scope: r.GetScope(),
}.Build())
}, wait.WithMaxTries(7))
require.NoError(t, err)
}
func createTCTLTerraformUserAndRole(t *testing.T, username string, instance *helpers.TeleInstance) {
// Test setup: creating a test user and its role
role, err := types.NewRole("test-role", types.RoleSpecV6{
@@ -218,6 +361,32 @@ func createTCTLTerraformUserAndRole(t *testing.T, username string, instance *hel
instance.AddUserWithRole(username, role)
}
func createTCTLTerraformCrossScopeAdmin(t *testing.T, username string, instance *helpers.TeleInstance) {
// Test setup: creating a test user and its role
role, err := types.NewRole("test-role-full-admin", types.RoleSpecV6{
Options: types.RoleOptions{},
Allow: types.RoleConditions{
Rules: []types.Rule{
{
Resources: []string{types.KindScopedToken},
Verbs: []string{types.VerbRead, types.VerbCreate, types.VerbList, types.VerbUpdate, types.VerbDelete},
},
{
Resources: []string{scopedaccess.KindScopedRole, scopedaccess.KindScopedRoleAssignment},
Verbs: []string{types.VerbReadNoSecrets, types.VerbCreate, types.VerbList, types.VerbUpdate, types.VerbDelete},
},
{
Resources: []string{types.KindBot},
Verbs: []string{types.VerbRead, types.VerbCreate, types.VerbList, types.VerbUpdate, types.VerbDelete},
},
},
},
})
require.NoError(t, err)
instance.AddUserWithRole(username, role)
}
// getAuthCLientForProxy builds an authclient.CLient connecting to the auth through the proxy
// (with a web client resolver hitting /v1/wenapi/ping and a tunnel auth dialer reaching the auth through the proxy).
// For the tests, the client is configured to trust the proxy TLS certs on first connection.
@@ -0,0 +1,22 @@
version: v1
kind: scoped_role
metadata:
name: terraform-provider
scope: "/"
spec:
assignable_scopes: ["/**"]
rules:
- resources: ["scoped_role", "scoped_role_assignment", "bot"]
verbs:
- list
- create
- readnosecrets
- update
- delete
- resources: ["scoped_token"]
verbs:
- list
- create
- read
- update
- delete
@@ -29,7 +29,6 @@ import (
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/jonboulle/clockwork"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/gravitational/teleport"
@@ -38,6 +37,7 @@ import (
scopedaccessv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/access/v1"
scopedjoiningv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/joining/v1"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/utils/wait"
"github.com/gravitational/teleport/integration/helpers"
"github.com/gravitational/teleport/integrations/lib/testing/integration"
"github.com/gravitational/teleport/lib/oidc/fakeissuer"
@@ -460,8 +460,8 @@ func TestTerraformJoinScoped(t *testing.T) {
// Test setup: Wait for the role assignment to enter the auth server cache
// else the bot will race against the cache, joining will fail and the test wil be flaky.
require.EventuallyWithT(t, func(t *assert.CollectT) {
resp, err := adminClient.ScopedAccessServiceClient().GetScopedRoleAssignment(
resp, err := wait.UntilFound(ctx, func(ctx context.Context) (*scopedaccessv1.GetScopedRoleAssignmentResponse, error) {
return adminClient.ScopedAccessServiceClient().GetScopedRoleAssignment(
ctx,
scopedaccessv1.GetScopedRoleAssignmentRequest_builder{
Name: testBotRole,
@@ -469,10 +469,10 @@ func TestTerraformJoinScoped(t *testing.T) {
SubKind: scopedaccess.SubKindDynamic,
}.Build(),
)
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, resp.GetAssignment())
}, 5*time.Second, 100*time.Millisecond, "expected role assignment to enter the auth cache")
})
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, resp.GetAssignment())
// Test setup: Create a fake Kubernetes JWKS signer that will be used by the bot to join the cluster.
const (
+219 -30
View File
@@ -26,6 +26,7 @@ import (
"io"
"log/slog"
"os"
"strconv"
"time"
"github.com/alecthomas/kingpin/v2"
@@ -36,12 +37,18 @@ import (
"github.com/gravitational/teleport/api/constants"
headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1"
machineidv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/machineid/v1"
scopedaccessv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/access/v1"
scopedjoiningv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/joining/v1"
"github.com/gravitational/teleport/api/identityfile"
"github.com/gravitational/teleport/api/mfa"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/types/common"
"github.com/gravitational/teleport/api/utils/wait"
"github.com/gravitational/teleport/lib/auth/authclient"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/scopes"
scopedaccess "github.com/gravitational/teleport/lib/scopes/access"
scopedjoining "github.com/gravitational/teleport/lib/scopes/joining"
"github.com/gravitational/teleport/lib/service/servicecfg"
"github.com/gravitational/teleport/lib/tbot/bot"
"github.com/gravitational/teleport/lib/tbot/bot/connection"
@@ -57,6 +64,7 @@ import (
)
const (
terraformScopedRoleDefault = "/::" + teleport.PresetTerraformProviderRoleName
terraformHelperDefaultResourcePrefix = "tctl-terraform-env-"
terraformHelperDefaultTTL = "1h"
@@ -75,6 +83,7 @@ type TerraformCommand struct {
resourcePrefix string
existingRole string
botTTL time.Duration
scope string
cfg *servicecfg.Config
@@ -105,6 +114,10 @@ func (c *TerraformCommand) Initialize(app *kingpin.Application, _ *tctlcfg.Globa
"role",
fmt.Sprintf("Role used by Terraform. The role must already exist in Teleport. When not specified, uses the default role %q", teleport.PresetTerraformProviderRoleName),
).StringVar(&c.existingRole)
c.envCmd.Flag(
"scope",
"Scope of the bot used by the Terraform provider. When empty, TF provider runs unscoped.",
).StringVar(&c.scope)
// Save a pointer to the config to be able to recover the Debug config later
c.cfg = cfg
@@ -163,46 +176,93 @@ func (c *TerraformCommand) RunEnvCommand(ctx context.Context, client *authclient
return trace.Wrap(err)
}
// Checking Terraform role
roleName, err := c.checkIfRoleExists(ctx, client)
if err != nil {
switch {
case trace.IsNotFound(err) && c.existingRole == "":
return trace.Wrap(err, `The Terraform role %q does not exist in your Teleport cluster.
var onboard onboarding.Config
scoped := c.scope != ""
if scoped {
// We are running in a scope.
roleSQN, err := c.checkIfScopedRoleExists(ctx, client.ScopedAccessServiceClient())
if err != nil {
if !trace.IsAccessDenied(err) {
return trace.Wrap(err, "checking if scoped role exists")
}
// We currently have no way for a scoped user to look into the assignable roles from parent scopes.
// So this check is likely to fail, even if the role exists.
// This should be revisited once we have an API to list such assignable parent roles.
}
token, assignment, err := c.createTransientScopedBotAndToken(ctx, client, roleSQN)
if err != nil {
return trace.Wrap(err, "creating transient scoped bot, token, and assigning role")
}
defer func() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
if _, err := client.ScopedAccessServiceClient().DeleteScopedRoleAssignment(ctx, scopedaccessv1.DeleteScopedRoleAssignmentRequest_builder{
Name: assignment.GetMetadata().GetName(),
Revision: assignment.GetMetadata().GetRevision(),
SubKind: assignment.GetSubKind(),
Scope: assignment.GetScope(),
}.Build()); err != nil {
c.showProgress(fmt.Sprintf("🚨 Failed to cleanup scoped role assignment %q", assignment.GetMetadata().GetName()))
}
cancel()
}()
onboard = onboarding.Config{
TokenValue: token.GetMetadata().GetName(),
JoinMethod: types.JoinMethodBoundKeypair,
BoundKeypair: onboarding.BoundKeypairOnboardingConfig{
RegistrationSecretValue: token.GetStatus().GetUsage().GetBoundKeypair().GetRegistrationSecret(),
},
}
} else {
// We are running unscoped.
// Checking Terraform role
roleName, err := c.checkIfRoleExists(ctx, client)
if err != nil {
switch {
case trace.IsNotFound(err) && c.existingRole == "":
return trace.Wrap(err, `The Terraform role %q does not exist in your Teleport cluster.
This default role is included in Teleport clusters whose version is higher than v16.1 or v17.
If you want to use "tctl terraform env" against an older Teleport cluster, you must create the Terraform role
yourself and set the flag --role <your-terraform-role-name>.`, roleName)
case trace.IsNotFound(err) && c.existingRole != "":
return trace.Wrap(err, `The Terraform role %q specified with --role does not exist in your Teleport cluster.
case trace.IsNotFound(err) && c.existingRole != "":
return trace.Wrap(err, `The Terraform role %q specified with --role does not exist in your Teleport cluster.
Please check that the role exists in the cluster.`, roleName)
case trace.IsAccessDenied(err):
return trace.Wrap(err, `Failed to validate if the role %q exists.
case trace.IsAccessDenied(err):
return trace.Wrap(err, `Failed to validate if the role %q exists.
To use the "tctl terraform env" command you must have rights to list and read Teleport roles.
If you got a role granted recently, you might have to run "tsh logout" and login again.`, roleName)
default:
return trace.Wrap(err, "Unexpected error while trying to validate if the role %q exists.", roleName)
default:
return trace.Wrap(err, "Unexpected error while trying to validate if the role %q exists.", roleName)
}
}
}
// Create temporary bot and token
tokenName, err := c.createTransientBotAndToken(ctx, client, roleName)
if trace.IsAccessDenied(err) {
return trace.Wrap(err, `Failed to create the temporary Terraform bot or its token.
// Create temporary bot and token
tokenName, err := c.createTransientBotAndToken(ctx, client, roleName)
if trace.IsAccessDenied(err) {
return trace.Wrap(err, `Failed to create the temporary Terraform bot or its token.
To use the "tctl terraform env" command you must have rights to create Teleport bot and token resources.
If you got a role granted recently, you might have to run "tsh logout" and login again.`)
}
if err != nil {
return trace.Wrap(err, "bootstrapping bot")
}
if err != nil {
return trace.Wrap(err, "bootstrapping bot")
}
onboard = onboarding.Config{
TokenValue: tokenName,
JoinMethod: types.JoinMethodToken,
}
}
// Now run tbot
c.showProgress("🤖 Using the temporary bot to obtain certificates")
id, sshHostCACerts, err := c.useBotToObtainIdentity(ctx, addr, tokenName, client)
id, sshHostCACerts, err := c.useBotToObtainIdentity(ctx, addr, onboard, client, scoped)
if err != nil {
return trace.Wrap(err, "The temporary bot failed to connect to Teleport.")
}
envVars, err := identityToTerraformEnvVars(addr.String(), id, sshHostCACerts)
envVars, err := identityToTerraformEnvVars(addr.String(), id, sshHostCACerts, scoped)
if err != nil {
return trace.Wrap(err, "exporting identity into environment variables")
}
@@ -274,6 +334,104 @@ func (c *TerraformCommand) createTransientBotAndToken(ctx context.Context, clien
return tokenName, nil
}
// createTransientBotAndToken creates a Bot resource and a secret Token.
// The token is single use (secret tokens are consumed on MachineID join)
// and the bot expires after the given TTL.
func (c *TerraformCommand) createTransientScopedBotAndToken(ctx context.Context, client *authclient.Client, roleName scopes.QualifiedName) (*scopedjoiningv1.ScopedToken, *scopedaccessv1.ScopedRoleAssignment, error) {
// Create token and bot name
suffix, err := utils.CryptoRandomHex(4)
if err != nil {
return nil, nil, trace.Wrap(err)
}
botName := c.resourcePrefix + suffix
c.showProgress(fmt.Sprintf("⚙️ Creating temporary bot %q and its token", botName))
token := scopedjoiningv1.ScopedToken_builder{
Kind: scopedaccess.KindScopedToken,
Version: types.V1,
Metadata: headerv1.Metadata_builder{
Name: botName,
Labels: terraformEnvCommandLabels,
Expires: timestamppb.New(time.Now().Add(c.botTTL)),
}.Build(),
Scope: c.scope,
Spec: scopedjoiningv1.ScopedTokenSpec_builder{
JoinMethod: string(types.JoinMethodBoundKeypair),
UsageMode: scopedjoining.TokenUsageModeBot,
Bot: scopes.QualifiedName{Scope: c.scope, Name: botName}.String(),
Roles: []string{string(types.RoleBot)},
BoundKeypair: scopedjoiningv1.BoundKeypairSpec_builder{Onboarding: scopedjoiningv1.BoundKeypairSpec_OnboardingSpec_builder{}.Build()}.Build(),
}.Build(),
}.Build()
token, err = client.CreateScopedToken(ctx, token)
if err != nil {
return nil, nil, trace.Wrap(err, "creating scoped token")
}
// Create bot
bot := machineidv1pb.Bot_builder{
Kind: types.KindBot,
Version: types.V1,
Metadata: headerv1.Metadata_builder{
Name: botName,
Expires: timestamppb.New(time.Now().Add(c.botTTL)),
Labels: terraformEnvCommandLabels,
}.Build(),
Scope: c.scope,
Spec: machineidv1pb.BotSpec_builder{}.Build(),
}.Build()
_, err = client.BotServiceClient().CreateBot(ctx, machineidv1pb.CreateBotRequest_builder{
Bot: bot,
}.Build())
if err != nil {
return token, nil, trace.Wrap(err, "creating bot")
}
// Assign role to bot
roleAssignment := scopedaccessv1.ScopedRoleAssignment_builder{
Kind: scopedaccess.KindScopedRoleAssignment,
SubKind: scopedaccess.SubKindDynamic,
Version: types.V1,
Metadata: headerv1.Metadata_builder{
Name: botName,
}.Build(),
Scope: c.scope,
Spec: scopedaccessv1.ScopedRoleAssignmentSpec_builder{
Assignments: []*scopedaccessv1.Assignment{
scopedaccessv1.Assignment_builder{
Role: roleName.String(),
Scope: c.scope,
}.Build(),
},
Bot: scopes.QualifiedName{Scope: c.scope, Name: botName}.String(),
}.Build(),
}.Build()
assignmentResponse, err := client.ScopedAccessServiceClient().CreateScopedRoleAssignment(
ctx,
scopedaccessv1.CreateScopedRoleAssignmentRequest_builder{
Assignment: roleAssignment,
}.Build())
if err != nil {
return token, nil, trace.Wrap(err, "creating scoped role assignment")
}
_, err = wait.UntilFound(ctx, func(ctx context.Context) (*scopedaccessv1.GetScopedRoleAssignmentResponse, error) {
return client.ScopedAccessServiceClient().GetScopedRoleAssignment(ctx,
scopedaccessv1.GetScopedRoleAssignmentRequest_builder{
Name: assignmentResponse.GetAssignment().GetMetadata().GetName(),
Scope: assignmentResponse.GetAssignment().GetScope(),
SubKind: assignmentResponse.GetAssignment().GetSubKind(),
}.Build())
})
if err != nil {
return token, nil, trace.Wrap(err, "waiting for role assignment to be created")
}
return token, assignmentResponse.GetAssignment(), nil
}
// roleClient describes the minimal set of operations that the helper uses to
// create the Terraform provider role.
type roleClient interface {
@@ -294,13 +452,40 @@ func (c *TerraformCommand) checkIfRoleExists(ctx context.Context, client roleCli
return roleName, trace.Wrap(err)
}
type scopedRoleClient interface {
GetScopedRole(ctx context.Context, req *scopedaccessv1.GetScopedRoleRequest) (*scopedaccessv1.GetScopedRoleResponse, error)
}
// createRoleIfNeeded checks if the terraform role exists.
// Returns the Terraform role name even in case of error, so this can be used to craft nice error messages.
func (c *TerraformCommand) checkIfScopedRoleExists(ctx context.Context, client scopedRoleClient) (scopes.QualifiedName, error) {
roleSQN := c.existingRole
if roleSQN == "" {
roleSQN = terraformScopedRoleDefault
}
sqn, err := scopes.ParseQualifiedName(roleSQN)
if err != nil {
return scopes.QualifiedName{}, trace.Wrap(err, "parsing role SQN %q", roleSQN)
}
req := scopedaccessv1.GetScopedRoleRequest_builder{
Name: sqn.Name,
Scope: sqn.Scope,
}.Build()
_, err = client.GetScopedRole(ctx, req)
return sqn, trace.Wrap(err)
}
// useBotToObtainIdentity takes secret bot token and runs a one-shot in-process tbot to trade the token
// against valid certificates. Those certs are then serialized into an identity file.
// The output is a set of environment variables, one of them including the base64-encoded identity file.
// Later, the Terraform provider will read those environment variables to build its Teleport client.
// Note: the function also returns the SSH Host CA cert encoded in the known host format.
// The identity.Identity uses a different format (authorized keys).
func (c *TerraformCommand) useBotToObtainIdentity(ctx context.Context, addr utils.NetAddr, token string, clt *authclient.Client) (*identity.Identity, [][]byte, error) {
func (c *TerraformCommand) useBotToObtainIdentity(ctx context.Context, addr utils.NetAddr, onboard onboarding.Config, clt *authclient.Client, scoped bool) (*identity.Identity, [][]byte, error) {
credential := &clientcredentials.UnstableConfig{}
credentialLifetime := bot.CredentialLifetime{
TTL: c.botTTL,
@@ -318,16 +503,14 @@ func (c *TerraformCommand) useBotToObtainIdentity(ctx context.Context, addr util
// This does not truly disable TLS validation, only trusts the certificate on first connection.
Insecure: clt.Config().InsecureSkipVerify,
},
Onboarding: onboarding.Config{
TokenValue: token,
JoinMethod: types.JoinMethodToken,
},
Onboarding: onboard,
InternalStorage: destination.NewMemory(),
CredentialLifetime: credentialLifetime,
Services: []bot.ServiceBuilder{
clientcredentials.ServiceBuilder(credential, credentialLifetime),
},
Logger: c.log,
Scoped: scoped,
}
// Insecure joining is not compatible with CA pinning
@@ -390,7 +573,7 @@ func (c *TerraformCommand) showProgress(update string) {
// identityToTerraformEnvVars takes an identity and builds environment variables
// configuring the Terraform provider to use this identity.
// The sshHostCACerts must be in the "known hosts" format.
func identityToTerraformEnvVars(addr string, id *identity.Identity, sshHostCACerts [][]byte) (map[string]string, error) {
func identityToTerraformEnvVars(addr string, id *identity.Identity, sshHostCACerts [][]byte, scoped bool) (map[string]string, error) {
idFile := &identityfile.IdentityFile{
PrivateKey: id.PrivateKeyBytes,
Certs: identityfile.Certs{
@@ -407,8 +590,14 @@ func identityToTerraformEnvVars(addr string, id *identity.Identity, sshHostCACer
return nil, trace.Wrap(err)
}
idBase64 := base64.StdEncoding.EncodeToString(idBytes)
return map[string]string{
envVars := map[string]string{
constants.EnvVarTerraformAddress: addr,
constants.EnvVarTerraformIdentityFileBase64: idBase64,
}, nil
}
if scoped {
envVars[constants.EnvVarTerraformScoped] = strconv.FormatBool(scoped)
}
return envVars, nil
}