Skip to content

Commit

Permalink
Adds tctl plugins install awsic
Browse files Browse the repository at this point in the history
Adds a comand-line installation tool for the AWS Identity Center integration.

Co-authored-by: Marek Smoliński <[email protected]>
  • Loading branch information
smallinsky authored and tcsc committed Jan 31, 2025
1 parent 724457a commit 367ac0a
Show file tree
Hide file tree
Showing 3 changed files with 311 additions and 0 deletions.
173 changes: 173 additions & 0 deletions tool/tctl/common/plugin/awsic.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
// Teleport
// Copyright (C) 2025 Gravitational, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

package plugin

import (
"context"
"fmt"

"github.com/alecthomas/kingpin/v2"
"github.com/gravitational/trace"

pluginspb "github.com/gravitational/teleport/api/gen/proto/go/teleport/plugins/v1"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/types/common"
"github.com/gravitational/teleport/lib/utils"
)

type awsICArgs struct {
cmd *kingpin.CmdClause
defaultOwners []string
scimToken string
scimURL string
region string
arn string
useSystemCredentials bool
groupNameFilters []string
accountNameFilters []string
accountIDFilters []string
}

func (a *awsICArgs) validate() error {
if !a.useSystemCredentials {
return trace.BadParameter("only AWS Local system credentials are supported")
}
return nil
}

// parseAWSICNameFilters validates that all elements of the supplied [names] slice
// are valid regexes or globs and wraps them in [types.AWSICResourceFilter]s for
// inclusion in a [types.PluginAWSICSettings].
//
// We are using a manual validator here rather than the canonical one defined
// in the AWS IC integration itself, because those filter tools are not
// available to OSS builds of tctl.
func parseAWSICNameFilters(names []string) ([]*types.AWSICResourceFilter, error) {
var filters []*types.AWSICResourceFilter
for _, n := range names {
if _, err := utils.CompileExpression(n); err != nil {
return nil, trace.Wrap(err)
}
filters = append(filters, &types.AWSICResourceFilter{
Include: &types.AWSICResourceFilter_NameRegex{NameRegex: n},
})
}
return filters, nil
}

func (a *awsICArgs) parseGroupFilters() ([]*types.AWSICResourceFilter, error) {
return parseAWSICNameFilters(a.groupNameFilters)
}

func (a *awsICArgs) parseAccountFilters() ([]*types.AWSICResourceFilter, error) {
filters, err := parseAWSICNameFilters(a.accountNameFilters)
if err != nil {
return nil, trace.Wrap(err)
}

for _, id := range a.accountIDFilters {
filters = append(filters, &types.AWSICResourceFilter{
Include: &types.AWSICResourceFilter_Id{Id: id},
})
}

return filters, nil
}

func (p *PluginsCommand) initInstallAWSIC(parent *kingpin.CmdClause) {
p.install.awsIC.cmd = parent.Command("awsic", "Install an AWS Identity Center integration.")
cmd := p.install.awsIC.cmd
cmd.Flag("default-owner", "List of Teleport users that are default owners for the imported access lists. Multiple flags allowed.").Required().StringsVar(&p.install.awsIC.defaultOwners)
cmd.Flag("url", "AWS Identity Center SCIM provisioning endpoint").Required().StringVar(&p.install.awsIC.scimURL)
cmd.Flag("token", "AWS Identify Center SCIM provisioning token.").Required().StringVar(&p.install.awsIC.scimToken)
cmd.Flag("region", "AWS Identity center instance region").Required().StringVar(&p.install.awsIC.region)
cmd.Flag("arn", "AWS Identify center instance ARN").Required().StringVar(&p.install.awsIC.arn)
cmd.Flag("use-system-credentials", "Uses system credentials instead of OIDC.").Default("true").BoolVar(&p.install.awsIC.useSystemCredentials)
cmd.Flag("group-filter", "Add AWS group to group import list by name. Can be a glob, or enclosed in ^$ to specify a regular expression. If no filters are supplied then all AWS groups will be imported.").
StringsVar(&p.install.awsIC.groupNameFilters)
cmd.Flag("account-name", "Add AWS Account to account import list by name. Can be a glob, or enclosed in ^$ to specify a regular expression. All AWS accounts will be imported if no items are added to account import list.").
StringsVar(&p.install.awsIC.accountNameFilters)
cmd.Flag("account-id", "Add AWS Account to account import list by ID. All AWS accounts will be imported if no items are added to account import list.").
StringsVar(&p.install.awsIC.accountIDFilters)
}

// InstallAWSIC installs AWS Identity Center plugin.
func (p *PluginsCommand) InstallAWSIC(ctx context.Context, args installPluginArgs) error {
awsICArgs := p.install.awsIC
if err := awsICArgs.validate(); err != nil {
return trace.Wrap(err)
}

groupFilters, err := awsICArgs.parseGroupFilters()
if err != nil {
return trace.Wrap(err)
}

accountFilters, err := awsICArgs.parseAccountFilters()
if err != nil {
return trace.Wrap(err)
}

req := &pluginspb.CreatePluginRequest{
Plugin: &types.PluginV1{
Metadata: types.Metadata{
Name: common.OriginAWSIdentityCenter,
Labels: map[string]string{
"teleport.dev/hosted-plugin": "true",
},
},
Spec: types.PluginSpecV1{
Settings: &types.PluginSpecV1_AwsIc{
AwsIc: &types.PluginAWSICSettings{
IntegrationName: common.OriginAWSIdentityCenter,
Region: awsICArgs.region,
Arn: awsICArgs.arn,
ProvisioningSpec: &types.AWSICProvisioningSpec{
BaseUrl: awsICArgs.scimURL,
},
AccessListDefaultOwners: awsICArgs.defaultOwners,
CredentialsSource: types.AWSICCredentialsSource_AWSIC_CREDENTIALS_SOURCE_SYSTEM,
GroupSyncFilters: groupFilters,
AwsAccountsFilters: accountFilters,
},
},
},
},
StaticCredentials: &types.PluginStaticCredentialsV1{
ResourceHeader: types.ResourceHeader{
Metadata: types.Metadata{
Labels: map[string]string{
"aws-ic/scim-api-endpoint": awsICArgs.scimURL,
},
Name: types.PluginTypeAWSIdentityCenter,
},
},
Spec: &types.PluginStaticCredentialsSpecV1{
Credentials: &types.PluginStaticCredentialsSpecV1_APIToken{
APIToken: awsICArgs.scimToken,
},
},
},
}

_, err = args.plugins.CreatePlugin(ctx, req)
if err != nil {
return trace.Wrap(err)
}
fmt.Println("Successfully created AWS Identity Center plugin.")
return nil
}
134 changes: 134 additions & 0 deletions tool/tctl/common/plugin/awsic_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// Teleport
// Copyright (C) 2025 Gravitational, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

package plugin

import (
"math/rand/v2"
"testing"

"github.com/stretchr/testify/require"

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

func TestAWSICGroupFilters(t *testing.T) {
t.Run("parse", func(t *testing.T) {
// GIVEN some arbitrary set of account name filter CLI args
cliArgs := awsICArgs{
groupNameFilters: []string{"alpha", "bravo", "charlie"},
}

// WHEN I attempt to convert the command-line args into filters
actualFilters, err := cliArgs.parseGroupFilters()

// EXPECT the operation to succeed
require.NoError(t, err)

// EXPECT that the returned filters are an accurate representation of
// the command line args, in arbitrary order
expectedFilters := []*types.AWSICResourceFilter{
{Include: &types.AWSICResourceFilter_NameRegex{NameRegex: "alpha"}},
{Include: &types.AWSICResourceFilter_NameRegex{NameRegex: "bravo"}},
{Include: &types.AWSICResourceFilter_NameRegex{NameRegex: "charlie"}},
}
rand.Shuffle(len(expectedFilters), func(i, j int) {
expectedFilters[i], expectedFilters[j] = expectedFilters[j], expectedFilters[i]
})
require.ElementsMatch(t, expectedFilters, actualFilters)
})

t.Run("empty lists are valid", func(*testing.T) {
// GIVEN a cli arg set that doesn't specify any group filters
cliArgs := awsICArgs{}

// WHEN I attempt to convert the command-line args into filters
filters, err := cliArgs.parseGroupFilters()

// EXPECT that the operation succeeds and returns an empty filter list
require.NoError(t, err)
require.Empty(t, filters)
})

t.Run("regex errors are detected", func(*testing.T) {
// GIVEN a name filter with a malformed regex
cliArgs := awsICArgs{
groupNameFilters: []string{"alpha", "^[)$", "charlie"},
}

// WHEN I attempt to convert the command-line args into filters
_, err := cliArgs.parseGroupFilters()

// EXPECT the operation to fail
require.Error(t, err)
})
}

func TestAWSICAccountFilters(t *testing.T) {
t.Run("parse", func(t *testing.T) {
// GIVEN some arbitrary combination of Name- and ID-based account filters
// CLI args..
cliArgs := awsICArgs{
accountNameFilters: []string{"alpha", "bravo", "charlie"},
accountIDFilters: []string{"0123456789", "9876543210"},
}

// WHEN I attempt to convert the command-line args into filters
actualFilters, err := cliArgs.parseAccountFilters()

// EXPECT the operation to succeed
require.NoError(t, err)

// EXPECT that the returned filters are an accurate representation of
// the command line args, in arbitrary order
expectedFilters := []*types.AWSICResourceFilter{
{Include: &types.AWSICResourceFilter_NameRegex{NameRegex: "alpha"}},
{Include: &types.AWSICResourceFilter_NameRegex{NameRegex: "bravo"}},
{Include: &types.AWSICResourceFilter_NameRegex{NameRegex: "charlie"}},
{Include: &types.AWSICResourceFilter_Id{Id: "0123456789"}},
{Include: &types.AWSICResourceFilter_Id{Id: "9876543210"}},
}
rand.Shuffle(len(expectedFilters), func(i, j int) {
expectedFilters[i], expectedFilters[j] = expectedFilters[j], expectedFilters[i]
})
require.ElementsMatch(t, expectedFilters, actualFilters)
})

t.Run("empty lists are valid", func(*testing.T) {
// GIVEN a cli arg set that doesn't specify any account filters
cliArgs := awsICArgs{}

// WHEN I attempt to convert the command-line args into filters
filters, err := cliArgs.parseAccountFilters()

// EXPECT that the operation succeeds and returns an empty filter list
require.NoError(t, err)
require.Empty(t, filters)
})

t.Run("regex errors are detected", func(*testing.T) {
// GIVEN a name filter with a malformed regex
cliArgs := awsICArgs{
accountNameFilters: []string{"alpha", "^[)$", "charlie"},
}

// WHEN I attempt to convert the command-line args into filters
_, err := cliArgs.parseAccountFilters()

// EXPECT the operation to fail
require.Error(t, err)
})
}
4 changes: 4 additions & 0 deletions tool/tctl/common/plugin/plugins_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ type pluginInstallArgs struct {
scim scimArgs
entraID entraArgs
netIQ netIQArgs
awsIC awsICArgs
}

type scimArgs struct {
Expand Down Expand Up @@ -104,6 +105,7 @@ func (p *PluginsCommand) initInstall(parent *kingpin.CmdClause, config *servicec
p.initInstallSCIM(p.install.cmd)
p.initInstallEntra(p.install.cmd)
p.initInstallNetIQ(p.install.cmd)
p.initInstallAWSIC(p.install.cmd)
}

func (p *PluginsCommand) initInstallSCIM(parent *kingpin.CmdClause) {
Expand Down Expand Up @@ -330,6 +332,8 @@ func (p *PluginsCommand) TryRun(ctx context.Context, cmd string, clientFunc comm
commandFunc = p.InstallEntra
case p.install.netIQ.cmd.FullCommand():
commandFunc = p.InstallNetIQ
case p.install.awsIC.cmd.FullCommand():
commandFunc = p.InstallAWSIC
case p.delete.cmd.FullCommand():
commandFunc = p.Delete
default:
Expand Down

0 comments on commit 367ac0a

Please sign in to comment.