mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-29 17:58:15 +08:00
feat: AWS OIDC integration role policy-preset for identity center integration (#47164)
* feat: adds a new 'policy-preset' flag to 'integration configure awsoidc-idp' command
* adapt to new awsactions
* output formatting and golden test
* run ui prettier, fix getAwsOidcConfigureIdpScriptUrl test
* comments cleanup, mend TestBuildAWSOIDCIdPConfigureScript test
* address review:
- use AwsOidcPolicyPreset in UrlAwsOidcConfigureIdp
- use NoSuchEntityException instead of EntityAlreadyExistsException in PutRolePolicy mock
- fix typo
* use empty string as unspecified value
* add optional field comment
* Merge branch 'master' of github.com:gravitational/teleport into sshah/aws-oidc-preset-policy
* Revert "Merge branch 'master' of github.com:gravitational/teleport into sshah/aws-oidc-preset-policy"
This reverts commit ad2a0904e9.
This commit is contained in:
@@ -433,3 +433,48 @@ func StatementAccessGraphAWSSync() *Statement {
|
||||
Resources: allResources,
|
||||
}
|
||||
}
|
||||
|
||||
// StatementForAWSIdentityCenterAccess returns AWS IAM policy statement that grants
|
||||
// permissions required for Teleport identity center client.
|
||||
// TODO(sshah): make the roles more granular by restricting resources scoped to
|
||||
// particular AWS identity center region+arn.
|
||||
func StatementForAWSIdentityCenterAccess() *Statement {
|
||||
return &Statement{
|
||||
StatementID: "TeleportIdentityCenterClient",
|
||||
Effect: EffectAllow,
|
||||
Actions: []string{
|
||||
// ListAccounts
|
||||
"organizations:ListAccounts",
|
||||
"organizations:ListAccountsForParent",
|
||||
|
||||
// ListGroupsAndMembers
|
||||
"identitystore:ListUsers",
|
||||
"identitystore:ListGroups",
|
||||
"identitystore:ListGroupMemberships",
|
||||
|
||||
// ListPermissionSetsAndAssignments
|
||||
"sso:DescribeInstance",
|
||||
"sso:DescribePermissionSet",
|
||||
"sso:ListPermissionSets",
|
||||
"sso:ListAccountAssignmentsForPrincipal",
|
||||
|
||||
// CreateAndDeleteAccountAssignment
|
||||
"sso:CreateAccountAssignment",
|
||||
"sso:DescribeAccountAssignmentCreationStatus",
|
||||
"sso:DeleteAccountAssignment",
|
||||
"sso:DescribeAccountAssignmentDeletionStatus",
|
||||
"iam:AttachRolePolicy",
|
||||
"iam:CreateRole",
|
||||
"iam:GetRole",
|
||||
"iam:ListAttachedRolePolicies",
|
||||
"iam:ListRolePolicies",
|
||||
|
||||
// AllowAccountAssignmentOnOwner
|
||||
"iam:GetSAMLProvider",
|
||||
|
||||
// ListProvisionedRoles
|
||||
"iam:ListRoles",
|
||||
},
|
||||
Resources: allResources,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Teleport
|
||||
* Copyright (C) 2024 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 awsactions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/service/iam"
|
||||
"github.com/gravitational/trace"
|
||||
|
||||
awslib "github.com/gravitational/teleport/lib/cloud/aws"
|
||||
"github.com/gravitational/teleport/lib/cloud/provisioning"
|
||||
)
|
||||
|
||||
// PolicyAssigner can assign a policy to an existing IAM role.
|
||||
type PolicyAssigner interface {
|
||||
// PutRolePolicy updates AWS IAM role with the given policy document.
|
||||
PutRolePolicy(ctx context.Context, params *iam.PutRolePolicyInput, optFns ...func(*iam.Options)) (*iam.PutRolePolicyOutput, error)
|
||||
}
|
||||
|
||||
// RolePolicy defines inputs for policy assignment request.
|
||||
type RolePolicy struct {
|
||||
// RoleName is the name of IAM role which will be assigned with the policy.
|
||||
RoleName string
|
||||
// PolicyName defines a new name for the policy.
|
||||
PolicyName string
|
||||
// PolicyStatement is a policy statement.
|
||||
PolicyStatement *awslib.Statement
|
||||
}
|
||||
|
||||
// AssignRolePolicy assigns AWS IAM role created by OIDC integration
|
||||
// with the given policy document derived from a preset policy statement.
|
||||
func AssignRolePolicy(
|
||||
clt interface {
|
||||
PolicyAssigner
|
||||
RoleGetter
|
||||
},
|
||||
req RolePolicy,
|
||||
) (*provisioning.Action, error) {
|
||||
policyDocument, err := awslib.NewPolicyDocument(
|
||||
req.PolicyStatement,
|
||||
).Marshal()
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
input := &iam.PutRolePolicyInput{
|
||||
PolicyName: &req.PolicyName,
|
||||
RoleName: &req.RoleName,
|
||||
PolicyDocument: &policyDocument,
|
||||
}
|
||||
|
||||
type assignPolicyInput struct {
|
||||
// PolicyDocument shadows the input's field of the same name
|
||||
// to marshal the policy statement as an unescpaed JSON.
|
||||
PolicyDocument *awslib.Statement
|
||||
*iam.PutRolePolicyInput
|
||||
}
|
||||
|
||||
details, err := formatDetails(assignPolicyInput{
|
||||
PolicyDocument: req.PolicyStatement,
|
||||
PutRolePolicyInput: input,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
config := provisioning.ActionConfig{
|
||||
Name: "AssignPolicy",
|
||||
Summary: fmt.Sprintf("Assign IAM role %q with an inline policy %q", req.RoleName, req.PolicyName),
|
||||
Details: details,
|
||||
RunnerFn: func(ctx context.Context) error {
|
||||
_, err = clt.PutRolePolicy(ctx, input)
|
||||
if err != nil {
|
||||
if trace.IsNotFound(awslib.ConvertIAMv2Error(err)) {
|
||||
return trace.NotFound("role %q not found.", req.RoleName)
|
||||
}
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
action, err := provisioning.NewAction(config)
|
||||
return action, trace.Wrap(err)
|
||||
}
|
||||
@@ -395,6 +395,9 @@ type IntegrationConfAWSOIDCIdP struct {
|
||||
ProxyPublicURL string
|
||||
// AutoConfirm skips user confirmation of the operation plan if true.
|
||||
AutoConfirm bool
|
||||
// PolicyPreset is the name of a pre-defined policy statement which will be
|
||||
// assigned to the AWS Role associate with the OIDC integration.
|
||||
PolicyPreset string
|
||||
}
|
||||
|
||||
// IntegrationConfListDatabasesIAM contains the arguments of
|
||||
|
||||
@@ -42,7 +42,7 @@ const (
|
||||
descriptionOIDCIdPRole = "Used by Teleport to provide access to AWS resources."
|
||||
)
|
||||
|
||||
// IdPIAMConfigureRequest is a request to configure the required Policies to use the EC2 Instance Connect Endpoint feature.
|
||||
// IdPIAMConfigureRequest represents a request to configure AWS OIDC integration.
|
||||
type IdPIAMConfigureRequest struct {
|
||||
// Cluster is the Teleport Cluster.
|
||||
// Used for tagging the created Roles/IdP.
|
||||
@@ -56,6 +56,14 @@ type IdPIAMConfigureRequest struct {
|
||||
// Used for tagging the created Roles/IdP.
|
||||
IntegrationName string
|
||||
|
||||
// IntegrationRole is the name of the AWS IAM role that will be created by the
|
||||
// AWS OIDC integration.
|
||||
IntegrationRole string
|
||||
|
||||
// IntegrationPolicyPreset is the name of a policy preset to be applied to the IntegrationRole.
|
||||
// Optional. If empty, no policy is assigned to the newly created IAM Role.
|
||||
IntegrationPolicyPreset PolicyPreset
|
||||
|
||||
// ProxyPublicAddress is the URL to use as provider URL.
|
||||
// This must be a valid URL (ie, url.Parse'able)
|
||||
// Eg, https://<tenant>.teleport.sh, https://proxy.example.org:443, https://teleport.ec2.aws:3080
|
||||
@@ -71,9 +79,6 @@ type IdPIAMConfigureRequest struct {
|
||||
// Eg, https://<tenant>.teleport.sh, https://proxy.example.org
|
||||
issuerURL string
|
||||
|
||||
// IntegrationRole is the Integration's AWS Role used to set up Teleport as an OIDC IdP.
|
||||
IntegrationRole string
|
||||
|
||||
ownershipTags tags.AWSTags
|
||||
|
||||
// stdout is used to override stdout output in tests.
|
||||
@@ -83,6 +88,33 @@ type IdPIAMConfigureRequest struct {
|
||||
fakeThumbprint string
|
||||
}
|
||||
|
||||
// PolicyPreset defines a preset policy type for the AWS IAM role
|
||||
// created by the Teleport AWS OIDC integration.
|
||||
type PolicyPreset string
|
||||
|
||||
const (
|
||||
// PolicyPresetUnspecified specifies no preset policy to apply.
|
||||
PolicyPresetUnspecified PolicyPreset = ""
|
||||
// PolicyPresetAWSIdentityCenter specifies poicy required for the AWS identity center integration.
|
||||
PolicyPresetAWSIdentityCenter PolicyPreset = "aws-identity-center"
|
||||
)
|
||||
|
||||
// ErrAWSOIDCInvalidPolicyPreset is issued if provided policy preset
|
||||
// value is not supported.
|
||||
var ErrAWSOIDCInvalidPolicyPreset = &trace.BadParameterError{
|
||||
Message: "--preset-policy defines an unknown preset value",
|
||||
}
|
||||
|
||||
// ValidatePolicyPreset validates if a given policy preset is supported or not.
|
||||
func ValidatePolicyPreset(input PolicyPreset) error {
|
||||
switch input {
|
||||
case PolicyPresetUnspecified, PolicyPresetAWSIdentityCenter:
|
||||
return nil
|
||||
default:
|
||||
return ErrAWSOIDCInvalidPolicyPreset
|
||||
}
|
||||
}
|
||||
|
||||
// CheckAndSetDefaults ensures the required fields are present.
|
||||
func (r *IdPIAMConfigureRequest) CheckAndSetDefaults() error {
|
||||
if r.Cluster == "" {
|
||||
@@ -113,6 +145,12 @@ func (r *IdPIAMConfigureRequest) CheckAndSetDefaults() error {
|
||||
|
||||
r.ownershipTags = tags.DefaultResourceCreationTags(r.Cluster, r.IntegrationName)
|
||||
|
||||
switch r.IntegrationPolicyPreset {
|
||||
case PolicyPresetUnspecified, PolicyPresetAWSIdentityCenter:
|
||||
default:
|
||||
return ErrAWSOIDCInvalidPolicyPreset
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -125,6 +163,7 @@ type IdPIAMConfigureClient interface {
|
||||
awsactions.RoleCreator
|
||||
awsactions.RoleGetter
|
||||
awsactions.RoleTagger
|
||||
awsactions.PolicyAssigner
|
||||
}
|
||||
|
||||
type defaultIdPIAMConfigureClient struct {
|
||||
@@ -160,10 +199,10 @@ func NewIdPIAMConfigureClient(ctx context.Context) (IdPIAMConfigureClient, error
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ConfigureIdPIAM creates a new IAM OIDC IdP in AWS.
|
||||
// ConfigureIdPIAM creates a new AWS IAM OIDC IdP, IAM role and optionally updates
|
||||
// the role with the given policy preset.
|
||||
//
|
||||
// The provider URL is Teleport's public address.
|
||||
// It also creates a new Role configured to trust the recently created IdP.
|
||||
// If the role already exists, it will create another trust relationship for the IdP (if it doesn't exist).
|
||||
//
|
||||
// The following actions must be allowed by the IAM Role assigned in the Client.
|
||||
@@ -194,12 +233,23 @@ func ConfigureIdPIAM(ctx context.Context, clt IdPIAMConfigureClient, req IdPIAMC
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
// --policy-preset is an optional flag so the assignIdPIAMPolicyAction
|
||||
// should only be appended if assignIdPIAMPolicyAction returns a non-nil value.
|
||||
actions := []provisioning.Action{
|
||||
*createOIDCIdP,
|
||||
*createIdPIAMRole,
|
||||
}
|
||||
assignIdPIAMPolicy, err := assignIdPIAMPolicyAction(clt, req)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
if assignIdPIAMPolicy != nil {
|
||||
actions = append(actions, *assignIdPIAMPolicy)
|
||||
}
|
||||
|
||||
return trace.Wrap(provisioning.Run(ctx, provisioning.OperationConfig{
|
||||
Name: "awsoidc-idp",
|
||||
Actions: []provisioning.Action{
|
||||
*createOIDCIdP,
|
||||
*createIdPIAMRole,
|
||||
},
|
||||
Name: "awsoidc-idp",
|
||||
Actions: actions,
|
||||
AutoConfirm: req.AutoConfirm,
|
||||
Output: req.stdout,
|
||||
}))
|
||||
@@ -234,3 +284,24 @@ func createIdPIAMRoleAction(clt IdPIAMConfigureClient, req IdPIAMConfigureReques
|
||||
req.ownershipTags,
|
||||
)
|
||||
}
|
||||
|
||||
func assignIdPIAMPolicyAction(clt IdPIAMConfigureClient, req IdPIAMConfigureRequest) (*provisioning.Action, error) {
|
||||
var policyName string
|
||||
var policyStatement *awslib.Statement
|
||||
|
||||
switch req.IntegrationPolicyPreset {
|
||||
case PolicyPresetAWSIdentityCenter:
|
||||
policyName = "TeleportAWSIdentityCenterIntegration"
|
||||
policyStatement = awslib.StatementForAWSIdentityCenterAccess()
|
||||
default:
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return awsactions.AssignRolePolicy(
|
||||
clt,
|
||||
awsactions.RolePolicy{
|
||||
RoleName: req.IntegrationRole,
|
||||
PolicyName: policyName,
|
||||
PolicyStatement: policyStatement,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/gravitational/teleport/lib"
|
||||
awslib "github.com/gravitational/teleport/lib/cloud/aws"
|
||||
"github.com/gravitational/teleport/lib/integrations/awsoidc/tags"
|
||||
"github.com/gravitational/teleport/lib/utils/golden"
|
||||
)
|
||||
@@ -42,11 +43,12 @@ import (
|
||||
func TestIdPIAMConfigReqDefaults(t *testing.T) {
|
||||
baseIdPIAMConfigReq := func() IdPIAMConfigureRequest {
|
||||
return IdPIAMConfigureRequest{
|
||||
Cluster: "mycluster",
|
||||
IntegrationName: "myintegration",
|
||||
IntegrationRole: "integrationrole",
|
||||
ProxyPublicAddress: "https://proxy.example.com",
|
||||
AutoConfirm: true,
|
||||
Cluster: "mycluster",
|
||||
IntegrationName: "myintegration",
|
||||
IntegrationRole: "integrationrole",
|
||||
ProxyPublicAddress: "https://proxy.example.com",
|
||||
IntegrationPolicyPreset: "",
|
||||
AutoConfirm: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +74,8 @@ func TestIdPIAMConfigReqDefaults(t *testing.T) {
|
||||
"teleport.dev/integration": "myintegration",
|
||||
"teleport.dev/origin": "integration_awsoidc",
|
||||
},
|
||||
AutoConfirm: true,
|
||||
IntegrationPolicyPreset: PolicyPresetUnspecified,
|
||||
AutoConfirm: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -111,6 +114,15 @@ func TestIdPIAMConfigReqDefaults(t *testing.T) {
|
||||
},
|
||||
errCheck: badParameterCheck,
|
||||
},
|
||||
{
|
||||
name: "invalid preset type",
|
||||
req: func() IdPIAMConfigureRequest {
|
||||
req := baseIdPIAMConfigReq()
|
||||
req.IntegrationPolicyPreset = "invalid_preset"
|
||||
return req
|
||||
},
|
||||
errCheck: badParameterCheck,
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
req := tt.req()
|
||||
@@ -296,19 +308,128 @@ func TestConfigureIdPIAM(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigureIdPIAMWithPresetPolicy(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
tlsServer := httptest.NewTLSServer(nil)
|
||||
lib.SetInsecureDevMode(true)
|
||||
defer lib.SetInsecureDevMode(false)
|
||||
const mockAccountID string = "123456789012"
|
||||
baseIdPIAMConfigReqWithTLServer := func() IdPIAMConfigureRequest {
|
||||
return IdPIAMConfigureRequest{
|
||||
Cluster: "mycluster",
|
||||
IntegrationName: "myintegration",
|
||||
IntegrationRole: "integrationrole",
|
||||
ProxyPublicAddress: tlsServer.URL,
|
||||
AutoConfirm: true,
|
||||
}
|
||||
}
|
||||
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
mockExistingRoles map[string]mockRole
|
||||
mockExistingIdPUrl []string
|
||||
req func() IdPIAMConfigureRequest
|
||||
errCheck require.ErrorAssertionFunc
|
||||
policyStatement *awslib.Statement
|
||||
externalStateCheck func(*testing.T, mockIdPIAMConfigClient)
|
||||
}{
|
||||
{
|
||||
name: "without policy-preset",
|
||||
req: func() IdPIAMConfigureRequest {
|
||||
req := baseIdPIAMConfigReqWithTLServer()
|
||||
req.IntegrationPolicyPreset = ""
|
||||
return req
|
||||
},
|
||||
mockExistingIdPUrl: []string{},
|
||||
mockExistingRoles: map[string]mockRole{},
|
||||
errCheck: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "with PolicyPresetUnspecified",
|
||||
req: func() IdPIAMConfigureRequest {
|
||||
req := baseIdPIAMConfigReqWithTLServer()
|
||||
req.IntegrationPolicyPreset = PolicyPresetUnspecified
|
||||
return req
|
||||
},
|
||||
mockExistingIdPUrl: []string{},
|
||||
mockExistingRoles: map[string]mockRole{},
|
||||
errCheck: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "with PolicyPresetAWSIdentityCenter",
|
||||
req: func() IdPIAMConfigureRequest {
|
||||
req := baseIdPIAMConfigReqWithTLServer()
|
||||
req.IntegrationPolicyPreset = PolicyPresetAWSIdentityCenter
|
||||
return req
|
||||
},
|
||||
mockExistingIdPUrl: []string{},
|
||||
mockExistingRoles: map[string]mockRole{},
|
||||
policyStatement: awslib.StatementForAWSIdentityCenterAccess(),
|
||||
errCheck: require.NoError,
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
clt := mockIdPIAMConfigClient{
|
||||
CallerIdentityGetter: mockSTSClient{accountID: mockAccountID},
|
||||
existingRoles: tt.mockExistingRoles,
|
||||
existingIDPUrl: tt.mockExistingIdPUrl,
|
||||
}
|
||||
|
||||
err := ConfigureIdPIAM(ctx, &clt, tt.req())
|
||||
tt.errCheck(t, err)
|
||||
|
||||
role, ok := clt.existingRoles[(tt.req().IntegrationRole)]
|
||||
require.True(t, ok)
|
||||
|
||||
if tt.req().IntegrationPolicyPreset == "" || tt.req().IntegrationPolicyPreset == PolicyPresetUnspecified {
|
||||
require.Nil(t, role.presetPolicyDoc)
|
||||
} else {
|
||||
policyDocument, err := awslib.NewPolicyDocument(
|
||||
tt.policyStatement,
|
||||
).Marshal()
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, role.presetPolicyDoc)
|
||||
require.Equal(t, &policyDocument, role.presetPolicyDoc)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var goldenIdPIAMConfigureRequest IdPIAMConfigureRequest = IdPIAMConfigureRequest{
|
||||
Cluster: "mycluster",
|
||||
IntegrationName: "myintegration",
|
||||
IntegrationRole: "integrationrole",
|
||||
ProxyPublicAddress: "https://example.com",
|
||||
AutoConfirm: true,
|
||||
fakeThumbprint: "15dbd260c7465ecca6de2c0b2181187f66ee0d1a",
|
||||
}
|
||||
|
||||
func TestConfigureIdPIAMOutput(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
var buf bytes.Buffer
|
||||
req := IdPIAMConfigureRequest{
|
||||
Cluster: "mycluster",
|
||||
IntegrationName: "myintegration",
|
||||
IntegrationRole: "integrationrole",
|
||||
ProxyPublicAddress: "https://example.com",
|
||||
AutoConfirm: true,
|
||||
stdout: &buf,
|
||||
fakeThumbprint: "15dbd260c7465ecca6de2c0b2181187f66ee0d1a",
|
||||
req := goldenIdPIAMConfigureRequest
|
||||
req.stdout = &buf
|
||||
|
||||
clt := mockIdPIAMConfigClient{
|
||||
CallerIdentityGetter: mockSTSClient{accountID: "123456789012"},
|
||||
existingRoles: map[string]mockRole{},
|
||||
existingIDPUrl: []string{},
|
||||
}
|
||||
|
||||
require.NoError(t, ConfigureIdPIAM(ctx, &clt, req))
|
||||
if golden.ShouldSet() {
|
||||
golden.Set(t, buf.Bytes())
|
||||
}
|
||||
require.Equal(t, string(golden.Get(t)), buf.String())
|
||||
}
|
||||
|
||||
func TestConfigureIdPIAMWithPolicyPresetOutput(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
var buf bytes.Buffer
|
||||
req := goldenIdPIAMConfigureRequest
|
||||
req.stdout = &buf
|
||||
req.IntegrationPolicyPreset = PolicyPresetAWSIdentityCenter
|
||||
|
||||
clt := mockIdPIAMConfigClient{
|
||||
CallerIdentityGetter: mockSTSClient{accountID: "123456789012"},
|
||||
existingRoles: map[string]mockRole{},
|
||||
@@ -325,6 +446,7 @@ func TestConfigureIdPIAMOutput(t *testing.T) {
|
||||
type mockRole struct {
|
||||
assumeRolePolicyDoc *string
|
||||
tags []iamtypes.Tag
|
||||
presetPolicyDoc *string
|
||||
}
|
||||
|
||||
type mockIdPIAMConfigClient struct {
|
||||
@@ -354,6 +476,24 @@ func (m *mockIdPIAMConfigClient) CreateRole(ctx context.Context, params *iam.Cre
|
||||
}, nil
|
||||
}
|
||||
|
||||
// PutRolePolicy assigns a policy to an existing IAM Role.
|
||||
func (m *mockIdPIAMConfigClient) PutRolePolicy(ctx context.Context, params *iam.PutRolePolicyInput, optFns ...func(*iam.Options)) (*iam.PutRolePolicyOutput, error) {
|
||||
doesNotExistMessage := fmt.Sprintf("Role %q does not exist.", *params.RoleName)
|
||||
if _, ok := m.existingRoles[aws.ToString(params.RoleName)]; !ok {
|
||||
return nil, &iamtypes.NoSuchEntityException{
|
||||
Message: &doesNotExistMessage,
|
||||
}
|
||||
}
|
||||
|
||||
m.existingRoles[*params.RoleName] = mockRole{
|
||||
tags: m.existingRoles[*params.RoleName].tags,
|
||||
assumeRolePolicyDoc: m.existingRoles[*params.RoleName].assumeRolePolicyDoc,
|
||||
presetPolicyDoc: params.PolicyDocument,
|
||||
}
|
||||
|
||||
return &iam.PutRolePolicyOutput{}, nil
|
||||
}
|
||||
|
||||
// CreateOpenIDConnectProvider creates an IAM OpenID Connect Provider.
|
||||
func (m *mockIdPIAMConfigClient) CreateOpenIDConnectProvider(ctx context.Context, params *iam.CreateOpenIDConnectProviderInput, optFns ...func(*iam.Options)) (*iam.CreateOpenIDConnectProviderOutput, error) {
|
||||
alreadyExistsMessage := fmt.Sprintf("IdP with URL %q already exists.", *params.Url)
|
||||
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
"awsoidc-idp" will perform the following actions:
|
||||
|
||||
1. Create an OpenID Connect identity provider in AWS IAM for your Teleport cluster.
|
||||
CreateOpenIDConnectProvider: {
|
||||
"Url": "https://example.com",
|
||||
"ClientIDList": [
|
||||
"discover.teleport"
|
||||
],
|
||||
"Tags": [
|
||||
{
|
||||
"Key": "teleport.dev/cluster",
|
||||
"Value": "mycluster"
|
||||
},
|
||||
{
|
||||
"Key": "teleport.dev/integration",
|
||||
"Value": "myintegration"
|
||||
},
|
||||
{
|
||||
"Key": "teleport.dev/origin",
|
||||
"Value": "integration_awsoidc"
|
||||
}
|
||||
],
|
||||
"ThumbprintList": [
|
||||
"15dbd260c7465ecca6de2c0b2181187f66ee0d1a"
|
||||
]
|
||||
}
|
||||
|
||||
2. Create IAM role "integrationrole" with a custom trust policy.
|
||||
CreateRole: {
|
||||
"AssumeRolePolicyDocument": {
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": "sts:AssumeRoleWithWebIdentity",
|
||||
"Principal": {
|
||||
"Federated": "arn:aws:iam::123456789012:oidc-provider/example.com"
|
||||
},
|
||||
"Condition": {
|
||||
"StringEquals": {
|
||||
"example.com:aud": "discover.teleport"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"RoleName": "integrationrole",
|
||||
"Description": "Used by Teleport to provide access to AWS resources.",
|
||||
"MaxSessionDuration": null,
|
||||
"Path": null,
|
||||
"PermissionsBoundary": null,
|
||||
"Tags": [
|
||||
{
|
||||
"Key": "teleport.dev/cluster",
|
||||
"Value": "mycluster"
|
||||
},
|
||||
{
|
||||
"Key": "teleport.dev/integration",
|
||||
"Value": "myintegration"
|
||||
},
|
||||
{
|
||||
"Key": "teleport.dev/origin",
|
||||
"Value": "integration_awsoidc"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
3. Assign IAM role "integrationrole" with an inline policy "TeleportAWSIdentityCenterIntegration".
|
||||
AssignPolicy: {
|
||||
"PolicyDocument": {
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"organizations:ListAccounts",
|
||||
"organizations:ListAccountsForParent",
|
||||
"identitystore:ListUsers",
|
||||
"identitystore:ListGroups",
|
||||
"identitystore:ListGroupMemberships",
|
||||
"sso:DescribeInstance",
|
||||
"sso:DescribePermissionSet",
|
||||
"sso:ListPermissionSets",
|
||||
"sso:ListAccountAssignmentsForPrincipal",
|
||||
"sso:CreateAccountAssignment",
|
||||
"sso:DescribeAccountAssignmentCreationStatus",
|
||||
"sso:DeleteAccountAssignment",
|
||||
"sso:DescribeAccountAssignmentDeletionStatus",
|
||||
"iam:AttachRolePolicy",
|
||||
"iam:CreateRole",
|
||||
"iam:GetRole",
|
||||
"iam:ListAttachedRolePolicies",
|
||||
"iam:ListRolePolicies",
|
||||
"iam:GetSAMLProvider",
|
||||
"iam:ListRoles"
|
||||
],
|
||||
"Resource": "*",
|
||||
"Sid": "TeleportIdentityCenterClient"
|
||||
},
|
||||
"PolicyName": "TeleportAWSIdentityCenterIntegration",
|
||||
"RoleName": "integrationrole"
|
||||
}
|
||||
|
||||
@@ -1114,6 +1114,13 @@ func (h *Handler) awsOIDCConfigureIdP(w http.ResponseWriter, r *http.Request, p
|
||||
fmt.Sprintf("--name=%s", shsprintf.EscapeDefaultContext(integrationName)),
|
||||
fmt.Sprintf("--role=%s", shsprintf.EscapeDefaultContext(role)),
|
||||
}
|
||||
policyPreset := queryParams.Get("policyPreset")
|
||||
if err := awsoidc.ValidatePolicyPreset(awsoidc.PolicyPreset(policyPreset)); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
if policyPreset != "" {
|
||||
argsList = append(argsList, fmt.Sprintf("--policy-preset=%s", shsprintf.EscapeDefaultContext(policyPreset)))
|
||||
}
|
||||
|
||||
// We have two set up modes:
|
||||
// - use the Proxy HTTP endpoint as Identity Provider
|
||||
|
||||
@@ -626,6 +626,7 @@ func TestBuildAWSOIDCIdPConfigureScript(t *testing.T) {
|
||||
"integrationName": []string{"myintegration"},
|
||||
"s3Bucket": []string{"my-bucket"},
|
||||
"s3Prefix": []string{"prefix"},
|
||||
"policyPreset": []string{""},
|
||||
},
|
||||
errCheck: require.NoError,
|
||||
expectedTeleportArgs: "integration configure awsoidc-idp " +
|
||||
@@ -666,6 +667,22 @@ func TestBuildAWSOIDCIdPConfigureScript(t *testing.T) {
|
||||
`--s3-bucket-uri=s3://my-bucket/prefix ` +
|
||||
"--s3-jwks-base64=" + jwksBase64,
|
||||
},
|
||||
{
|
||||
name: "valid with supported policy preset",
|
||||
reqQuery: url.Values{
|
||||
"awsRegion": []string{"us-east-1"},
|
||||
"role": []string{"myRole"},
|
||||
"integrationName": []string{"myintegration"},
|
||||
"policyPreset": []string{"aws-identity-center"},
|
||||
},
|
||||
errCheck: require.NoError,
|
||||
expectedTeleportArgs: "integration configure awsoidc-idp " +
|
||||
"--cluster=localhost " +
|
||||
"--name=myintegration " +
|
||||
"--role=myRole " +
|
||||
"--policy-preset=aws-identity-center " +
|
||||
"--proxy-public-url=" + proxyPublicURL.String(),
|
||||
},
|
||||
{
|
||||
name: "missing role",
|
||||
reqQuery: url.Values{
|
||||
@@ -702,6 +719,15 @@ func TestBuildAWSOIDCIdPConfigureScript(t *testing.T) {
|
||||
},
|
||||
errCheck: isBadParamErrFn,
|
||||
},
|
||||
{
|
||||
name: "missing policy preset",
|
||||
reqQuery: url.Values{
|
||||
"integrationName": []string{"myintegration"},
|
||||
"role": []string{"role"},
|
||||
"s3Bucket": []string{"my-bucket"},
|
||||
},
|
||||
errCheck: isBadParamErrFn,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
|
||||
@@ -152,11 +152,12 @@ func onIntegrationConfAWSOIDCIdP(ctx context.Context, clf config.CommandLineFlag
|
||||
}
|
||||
|
||||
confReq := awsoidc.IdPIAMConfigureRequest{
|
||||
Cluster: clf.IntegrationConfAWSOIDCIdPArguments.Cluster,
|
||||
IntegrationName: clf.IntegrationConfAWSOIDCIdPArguments.Name,
|
||||
IntegrationRole: clf.IntegrationConfAWSOIDCIdPArguments.Role,
|
||||
ProxyPublicAddress: clf.IntegrationConfAWSOIDCIdPArguments.ProxyPublicURL,
|
||||
AutoConfirm: clf.IntegrationConfAWSOIDCIdPArguments.AutoConfirm,
|
||||
Cluster: clf.IntegrationConfAWSOIDCIdPArguments.Cluster,
|
||||
IntegrationName: clf.IntegrationConfAWSOIDCIdPArguments.Name,
|
||||
IntegrationRole: clf.IntegrationConfAWSOIDCIdPArguments.Role,
|
||||
ProxyPublicAddress: clf.IntegrationConfAWSOIDCIdPArguments.ProxyPublicURL,
|
||||
IntegrationPolicyPreset: awsoidc.PolicyPreset(clf.IntegrationConfAWSOIDCIdPArguments.PolicyPreset),
|
||||
AutoConfirm: clf.IntegrationConfAWSOIDCIdPArguments.AutoConfirm,
|
||||
}
|
||||
return trace.Wrap(awsoidc.ConfigureIdPIAM(ctx, iamClient, confReq))
|
||||
}
|
||||
|
||||
@@ -521,6 +521,8 @@ func Run(options Options) (app *kingpin.Application, executedCommand string, con
|
||||
IntegrationConfAWSOIDCIdPArguments.Role)
|
||||
integrationConfAWSOIDCIdPCmd.Flag("proxy-public-url", "Proxy Public URL (eg https://mytenant.teleport.sh).").Required().StringVar(&ccf.
|
||||
IntegrationConfAWSOIDCIdPArguments.ProxyPublicURL)
|
||||
integrationConfAWSOIDCIdPCmd.Flag("policy-preset", "Policy that will be applied to the AWS OIDC integration role.").StringVar(&ccf.
|
||||
IntegrationConfAWSOIDCIdPArguments.PolicyPreset)
|
||||
integrationConfAWSOIDCIdPCmd.Flag("confirm", "Apply changes without confirmation prompt.").BoolVar(&ccf.IntegrationConfAWSOIDCIdPArguments.AutoConfirm)
|
||||
integrationConfAWSOIDCIdPCmd.Flag("insecure", "Insecure mode disables certificate validation.").BoolVar(&ccf.InsecureMode)
|
||||
|
||||
|
||||
@@ -42,7 +42,10 @@ import { TextSelectCopyMulti } from 'shared/components/TextSelectCopy';
|
||||
|
||||
import { FieldCheckbox } from 'shared/components/FieldCheckbox';
|
||||
|
||||
import { Integration } from 'teleport/services/integrations';
|
||||
import {
|
||||
Integration,
|
||||
AwsOidcPolicyPreset,
|
||||
} from 'teleport/services/integrations';
|
||||
import cfg from 'teleport/config';
|
||||
import { splitAwsIamArn } from 'teleport/services/integrations/aws';
|
||||
|
||||
@@ -71,7 +74,10 @@ export function EditAwsOidcIntegrationDialog(props: Props) {
|
||||
run(() => edit({ roleArn }));
|
||||
}
|
||||
|
||||
function generateAwsOidcConfigIdpScript(validator: Validator) {
|
||||
function generateAwsOidcConfigIdpScript(
|
||||
validator: Validator,
|
||||
policyPreset: AwsOidcPolicyPreset
|
||||
) {
|
||||
if (!validator.validate()) {
|
||||
return;
|
||||
}
|
||||
@@ -84,6 +90,7 @@ export function EditAwsOidcIntegrationDialog(props: Props) {
|
||||
const newScriptUrl = cfg.getAwsOidcConfigureIdpScriptUrl({
|
||||
integrationName: integration.name,
|
||||
roleName: arnResourceName,
|
||||
policyPreset,
|
||||
});
|
||||
|
||||
setScriptUrl(newScriptUrl);
|
||||
@@ -209,7 +216,12 @@ export function EditAwsOidcIntegrationDialog(props: Props) {
|
||||
{!scriptUrl && showGenerateCommand && (
|
||||
<ButtonBorder
|
||||
mb={3}
|
||||
onClick={() => generateAwsOidcConfigIdpScript(validator)}
|
||||
onClick={() =>
|
||||
generateAwsOidcConfigIdpScript(
|
||||
validator,
|
||||
AwsOidcPolicyPreset.Unspecified
|
||||
)
|
||||
}
|
||||
disabled={!roleArn}
|
||||
>
|
||||
Reconfigure
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
} from 'teleport/Integrations/shared';
|
||||
import { AWS_RESOURCE_GROUPS_TAG_EDITOR_LINK } from 'teleport/Discover/Shared/const';
|
||||
import useStickyClusterId from 'teleport/useStickyClusterId';
|
||||
import { AwsOidcPolicyPreset } from 'teleport/services/integrations';
|
||||
|
||||
import { FinishDialog } from './FinishDialog';
|
||||
import { useAwsOidcIntegration } from './useAwsOidcIntegration';
|
||||
@@ -146,7 +147,12 @@ export function AwsOidc() {
|
||||
) : (
|
||||
<ButtonSecondary
|
||||
mb={3}
|
||||
onClick={() => generateAwsOidcConfigIdpScript(validator)}
|
||||
onClick={() =>
|
||||
generateAwsOidcConfigIdpScript(
|
||||
validator,
|
||||
AwsOidcPolicyPreset.Unspecified
|
||||
)
|
||||
}
|
||||
>
|
||||
Generate Command
|
||||
</ButtonSecondary>
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
IntegrationCreateRequest,
|
||||
IntegrationKind,
|
||||
integrationService,
|
||||
AwsOidcPolicyPreset,
|
||||
} from 'teleport/services/integrations';
|
||||
|
||||
type integrationConfig = {
|
||||
@@ -109,7 +110,10 @@ export function useAwsOidcIntegration() {
|
||||
emitEvent(IntegrationEnrollEvent.Complete);
|
||||
}
|
||||
|
||||
function generateAwsOidcConfigIdpScript(validator: Validator) {
|
||||
function generateAwsOidcConfigIdpScript(
|
||||
validator: Validator,
|
||||
policyPreset: AwsOidcPolicyPreset
|
||||
) {
|
||||
if (!validator.validate()) {
|
||||
return;
|
||||
}
|
||||
@@ -119,6 +123,7 @@ export function useAwsOidcIntegration() {
|
||||
const newScriptUrl = cfg.getAwsOidcConfigureIdpScriptUrl({
|
||||
integrationName: integrationConfig.name,
|
||||
roleName: integrationConfig.roleName,
|
||||
policyPreset,
|
||||
});
|
||||
|
||||
setScriptUrl(newScriptUrl);
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { AwsOidcPolicyPreset } from 'teleport/services/integrations';
|
||||
|
||||
import cfg, {
|
||||
UrlDeployServiceIamConfigureScriptParams,
|
||||
UrlAwsOidcConfigureIdp,
|
||||
@@ -42,10 +44,11 @@ test('getAwsOidcConfigureIdpScriptUrl formatting, without s3 fields', async () =
|
||||
const params: UrlAwsOidcConfigureIdp = {
|
||||
integrationName: 'int-name',
|
||||
roleName: 'role-arn',
|
||||
policyPreset: AwsOidcPolicyPreset.Unspecified,
|
||||
};
|
||||
const base =
|
||||
'http://localhost/v1/webapi/scripts/integrations/configure/awsoidc-idp.sh?';
|
||||
const expected = `integrationName=int-name&role=role-arn`;
|
||||
const expected = `integrationName=int-name&role=role-arn&policyPreset=`;
|
||||
expect(cfg.getAwsOidcConfigureIdpScriptUrl(params)).toBe(
|
||||
`${base}${expected}`
|
||||
);
|
||||
|
||||
@@ -35,7 +35,11 @@ import type {
|
||||
import type { SortType } from 'teleport/services/agents';
|
||||
import type { RecordingType } from 'teleport/services/recordings';
|
||||
import type { WebauthnAssertionResponse } from './services/auth';
|
||||
import type { PluginKind, Regions } from './services/integrations';
|
||||
import type {
|
||||
PluginKind,
|
||||
Regions,
|
||||
AwsOidcPolicyPreset,
|
||||
} from './services/integrations';
|
||||
import type { ParticipantMode } from 'teleport/services/session';
|
||||
import type { YamlSupportedResourceKind } from './services/yaml/types';
|
||||
import type { KubeResourceKind } from './services/kube/types';
|
||||
@@ -309,7 +313,7 @@ const cfg = {
|
||||
thumbprintPath: '/v1/webapi/thumbprint',
|
||||
|
||||
awsConfigureIamScriptOidcIdpPath:
|
||||
'/v1/webapi/scripts/integrations/configure/awsoidc-idp.sh?integrationName=:integrationName&role=:roleName',
|
||||
'/v1/webapi/scripts/integrations/configure/awsoidc-idp.sh?integrationName=:integrationName&role=:roleName&policyPreset=:policyPreset?',
|
||||
awsConfigureIamScriptDeployServicePath:
|
||||
'/v1/webapi/scripts/integrations/configure/deployservice-iam.sh?integrationName=:integrationName&awsRegion=:region&role=:awsOidcRoleArn&taskRole=:taskRoleArn&awsAccountID=:accountID',
|
||||
awsConfigureIamScriptListDatabasesPath:
|
||||
@@ -545,7 +549,10 @@ const cfg = {
|
||||
|
||||
getAwsOidcConfigureIdpScriptUrl(p: UrlAwsOidcConfigureIdp) {
|
||||
const path = cfg.api.awsConfigureIamScriptOidcIdpPath;
|
||||
return cfg.baseUrl + generatePath(path, { ...p });
|
||||
return (
|
||||
cfg.baseUrl +
|
||||
generatePath(path, { ...p, policyPreset: p.policyPreset || undefined })
|
||||
);
|
||||
},
|
||||
|
||||
getDbScriptUrl(token: string) {
|
||||
@@ -1267,6 +1274,7 @@ export interface UrlAwsOidcConfigureIdp {
|
||||
roleName: string;
|
||||
s3Bucket?: string;
|
||||
s3Prefix?: string;
|
||||
policyPreset?: AwsOidcPolicyPreset;
|
||||
}
|
||||
|
||||
export interface UrlAwsConfigureIamScriptParams {
|
||||
|
||||
@@ -373,6 +373,15 @@ export type AwsOidcDeployServiceRequest = {
|
||||
accountId: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* AwsOidcPolicyPreset specifies preset policy to apply
|
||||
* to the AWS IAM role created for the OIDC integration.
|
||||
*/
|
||||
export enum AwsOidcPolicyPreset {
|
||||
Unspecified = '',
|
||||
AwsIdentityCenter = 'aws-identity-center',
|
||||
}
|
||||
|
||||
// DeployDatabaseServiceDeployment identifies the required fields to deploy a DatabaseService.
|
||||
type DeployDatabaseServiceDeployment = {
|
||||
// VPCID is the VPCID where the service is going to be deployed.
|
||||
|
||||
Reference in New Issue
Block a user