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:
Sakshyam Shah
2024-10-08 15:13:40 +00:00
committed by GitHub
parent 47c22ee10e
commit 3eaab34ff2
16 changed files with 579 additions and 39 deletions
+45
View File
@@ -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)
}
+3
View File
@@ -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
+82 -11
View File
@@ -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,
})
}
+154 -14
View File
@@ -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)
@@ -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"
}
+7
View File
@@ -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
+26
View File
@@ -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))
}
+2
View File
@@ -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);
+4 -1
View File
@@ -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}`
);
+11 -3
View File
@@ -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.