From 3eaab34ff29766ef449a9e2b342fa9351c813e1c Mon Sep 17 00:00:00 2001 From: Sakshyam Shah Date: Tue, 8 Oct 2024 11:13:40 -0400 Subject: [PATCH] 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 ad2a0904e9076f39e468083b906f101b1171283b. --- lib/cloud/aws/policy_statements.go | 45 +++++ .../provisioning/awsactions/assign_policy.go | 102 +++++++++++ lib/config/configuration.go | 3 + lib/integrations/awsoidc/idp_iam_config.go | 93 ++++++++-- .../awsoidc/idp_iam_config_test.go | 168 ++++++++++++++++-- ...nfigureIdPIAMWithPolicyPresetOutput.golden | 100 +++++++++++ lib/web/integrations_awsoidc.go | 7 + lib/web/integrations_awsoidc_test.go | 26 +++ tool/teleport/common/integration_configure.go | 11 +- tool/teleport/common/teleport.go | 2 + .../EditAwsOidcIntegrationDialog.tsx | 18 +- .../Integrations/Enroll/AwsOidc/AwsOidc.tsx | 8 +- .../Enroll/AwsOidc/useAwsOidcIntegration.tsx | 7 +- web/packages/teleport/src/config.test.ts | 5 +- web/packages/teleport/src/config.ts | 14 +- .../src/services/integrations/types.ts | 9 + 16 files changed, 579 insertions(+), 39 deletions(-) create mode 100644 lib/cloud/provisioning/awsactions/assign_policy.go create mode 100644 lib/integrations/awsoidc/testdata/TestConfigureIdPIAMWithPolicyPresetOutput.golden diff --git a/lib/cloud/aws/policy_statements.go b/lib/cloud/aws/policy_statements.go index 644981e3ccd..2e391b4fc13 100644 --- a/lib/cloud/aws/policy_statements.go +++ b/lib/cloud/aws/policy_statements.go @@ -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, + } +} diff --git a/lib/cloud/provisioning/awsactions/assign_policy.go b/lib/cloud/provisioning/awsactions/assign_policy.go new file mode 100644 index 00000000000..b6dab6434df --- /dev/null +++ b/lib/cloud/provisioning/awsactions/assign_policy.go @@ -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 . + */ + +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) +} diff --git a/lib/config/configuration.go b/lib/config/configuration.go index 10d46a2fd2c..f0f339a6ec9 100644 --- a/lib/config/configuration.go +++ b/lib/config/configuration.go @@ -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 diff --git a/lib/integrations/awsoidc/idp_iam_config.go b/lib/integrations/awsoidc/idp_iam_config.go index 6fff791c34b..e02e7ef3632 100644 --- a/lib/integrations/awsoidc/idp_iam_config.go +++ b/lib/integrations/awsoidc/idp_iam_config.go @@ -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://.teleport.sh, https://proxy.example.org:443, https://teleport.ec2.aws:3080 @@ -71,9 +79,6 @@ type IdPIAMConfigureRequest struct { // Eg, https://.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, + }) +} diff --git a/lib/integrations/awsoidc/idp_iam_config_test.go b/lib/integrations/awsoidc/idp_iam_config_test.go index a7430d1bee9..7961fc82552 100644 --- a/lib/integrations/awsoidc/idp_iam_config_test.go +++ b/lib/integrations/awsoidc/idp_iam_config_test.go @@ -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) diff --git a/lib/integrations/awsoidc/testdata/TestConfigureIdPIAMWithPolicyPresetOutput.golden b/lib/integrations/awsoidc/testdata/TestConfigureIdPIAMWithPolicyPresetOutput.golden new file mode 100644 index 00000000000..1090d1a7dc3 --- /dev/null +++ b/lib/integrations/awsoidc/testdata/TestConfigureIdPIAMWithPolicyPresetOutput.golden @@ -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" +} + diff --git a/lib/web/integrations_awsoidc.go b/lib/web/integrations_awsoidc.go index fb9d661d305..01daac8b3f4 100644 --- a/lib/web/integrations_awsoidc.go +++ b/lib/web/integrations_awsoidc.go @@ -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 diff --git a/lib/web/integrations_awsoidc_test.go b/lib/web/integrations_awsoidc_test.go index 27a7708e3ea..200d68ef7d9 100644 --- a/lib/web/integrations_awsoidc_test.go +++ b/lib/web/integrations_awsoidc_test.go @@ -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 { diff --git a/tool/teleport/common/integration_configure.go b/tool/teleport/common/integration_configure.go index b79d285010d..bfd762d1322 100644 --- a/tool/teleport/common/integration_configure.go +++ b/tool/teleport/common/integration_configure.go @@ -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)) } diff --git a/tool/teleport/common/teleport.go b/tool/teleport/common/teleport.go index a7f4de96754..3ccaa6ad192 100644 --- a/tool/teleport/common/teleport.go +++ b/tool/teleport/common/teleport.go @@ -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) diff --git a/web/packages/teleport/src/Integrations/EditAwsOidcIntegrationDialog.tsx b/web/packages/teleport/src/Integrations/EditAwsOidcIntegrationDialog.tsx index f38592a62cc..acc5cfbb459 100644 --- a/web/packages/teleport/src/Integrations/EditAwsOidcIntegrationDialog.tsx +++ b/web/packages/teleport/src/Integrations/EditAwsOidcIntegrationDialog.tsx @@ -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 && ( generateAwsOidcConfigIdpScript(validator)} + onClick={() => + generateAwsOidcConfigIdpScript( + validator, + AwsOidcPolicyPreset.Unspecified + ) + } disabled={!roleArn} > Reconfigure diff --git a/web/packages/teleport/src/Integrations/Enroll/AwsOidc/AwsOidc.tsx b/web/packages/teleport/src/Integrations/Enroll/AwsOidc/AwsOidc.tsx index b823bd87df4..a1c4a62b185 100644 --- a/web/packages/teleport/src/Integrations/Enroll/AwsOidc/AwsOidc.tsx +++ b/web/packages/teleport/src/Integrations/Enroll/AwsOidc/AwsOidc.tsx @@ -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() { ) : ( generateAwsOidcConfigIdpScript(validator)} + onClick={() => + generateAwsOidcConfigIdpScript( + validator, + AwsOidcPolicyPreset.Unspecified + ) + } > Generate Command diff --git a/web/packages/teleport/src/Integrations/Enroll/AwsOidc/useAwsOidcIntegration.tsx b/web/packages/teleport/src/Integrations/Enroll/AwsOidc/useAwsOidcIntegration.tsx index 0eb98758b80..25ff90b42df 100644 --- a/web/packages/teleport/src/Integrations/Enroll/AwsOidc/useAwsOidcIntegration.tsx +++ b/web/packages/teleport/src/Integrations/Enroll/AwsOidc/useAwsOidcIntegration.tsx @@ -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); diff --git a/web/packages/teleport/src/config.test.ts b/web/packages/teleport/src/config.test.ts index 168f543ef2a..e8e9799c2aa 100644 --- a/web/packages/teleport/src/config.test.ts +++ b/web/packages/teleport/src/config.test.ts @@ -16,6 +16,8 @@ * along with this program. If not, see . */ +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}` ); diff --git a/web/packages/teleport/src/config.ts b/web/packages/teleport/src/config.ts index 977bfbc480d..7f2837524c2 100644 --- a/web/packages/teleport/src/config.ts +++ b/web/packages/teleport/src/config.ts @@ -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 { diff --git a/web/packages/teleport/src/services/integrations/types.ts b/web/packages/teleport/src/services/integrations/types.ts index 07c794353d2..c4eb8a98be0 100644 --- a/web/packages/teleport/src/services/integrations/types.ts +++ b/web/packages/teleport/src/services/integrations/types.ts @@ -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.