-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adds a comand-line installation tool for the AWS Identity Center integration. Co-authored-by: Marek Smoliński <[email protected]>
- Loading branch information
1 parent
724457a
commit 367ac0a
Showing
3 changed files
with
311 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters