-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
make configure awsoidc-idp actions transparent
Applies to the integration command that the web UI discover flows tell users to run in AWS CloudShell to setup the AWS OIDC identity provider: teleport integration configure awsoidc-idp The command describes itself, its actions, and the desired state after it runs. It then prompts the user (by default) to confirm the action plan before proceeding. The confirmation prompt can be overridden with cli flag --confirm if desired. The IAM role it configures is no longer required to have the "ownership" tags that teleport applies if it's created by teleport, since the user is now prompted for confirmation before making changes. This allows a user to configure an existing IAM role without tagging the role for configuration by teleport. The command will still attempt to ensure the IAM role it configures has teleport tags, but failing to do so is only a warning.
- Loading branch information
1 parent
b62dfd8
commit da5197f
Showing
10 changed files
with
1,135 additions
and
108 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
/* | ||
* 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 ( | ||
"encoding/json" | ||
|
||
"github.com/gravitational/trace" | ||
) | ||
|
||
func formatDetails(in any) (string, error) { | ||
const prefix = "" | ||
const indent = " " | ||
out, err := json.MarshalIndent(in, prefix, indent) | ||
return string(out), trace.Wrap(err) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
/* | ||
* 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" | ||
"log/slog" | ||
|
||
"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" | ||
"github.com/gravitational/teleport/lib/integrations/awsoidc/tags" | ||
) | ||
|
||
// OpenIDConnectProviderCreator can create an OpenID Connect Identity Provider | ||
// (OIDC IdP) in AWS IAM. | ||
type OpenIDConnectProviderCreator interface { | ||
// CreateOpenIDConnectProvider creates an AWS IAM OIDC IdP. | ||
CreateOpenIDConnectProvider(ctx context.Context, params *iam.CreateOpenIDConnectProviderInput, optFns ...func(*iam.Options)) (*iam.CreateOpenIDConnectProviderOutput, error) | ||
} | ||
|
||
// CreateOIDCProvider wraps a [OpenIDConnectPRoviderCreator] in a | ||
// [provisioning.Action] that creates an OIDC IdP in AWS IAM when invoked. | ||
func CreateOIDCProvider( | ||
clt OpenIDConnectProviderCreator, | ||
thumbprints []string, | ||
issuerURL string, | ||
clientIDs []string, | ||
tags tags.AWSTags, | ||
) (*provisioning.Action, error) { | ||
details, err := formatDetails(identityProviderDetails{ | ||
IssuerURL: issuerURL, | ||
ClientIDs: clientIDs, | ||
Thumbprints: thumbprints, | ||
}) | ||
if err != nil { | ||
return nil, trace.Wrap(err) | ||
} | ||
|
||
config := provisioning.ActionConfig{ | ||
Name: "CreateOpenIDConnectIdentityProvider", | ||
Summary: "Create an OpenID Connect identity provider in AWS IAM for your Teleport cluster", | ||
Details: details, | ||
RunnerFn: func(ctx context.Context) error { | ||
slog.InfoContext(ctx, "Creating OpenID Connect identity provider", | ||
"issuer_url", issuerURL, | ||
) | ||
_, err = clt.CreateOpenIDConnectProvider(ctx, &iam.CreateOpenIDConnectProviderInput{ | ||
ThumbprintList: thumbprints, | ||
Url: &issuerURL, | ||
ClientIDList: clientIDs, | ||
Tags: tags.ToIAMTags(), | ||
}) | ||
if err != nil { | ||
awsErr := awslib.ConvertIAMv2Error(err) | ||
if trace.IsAlreadyExists(awsErr) { | ||
slog.InfoContext(ctx, "OpenID Connect identity provider already exists", | ||
"issuer_url", issuerURL, | ||
) | ||
return nil | ||
} | ||
|
||
return trace.Wrap(err) | ||
} | ||
return nil | ||
}, | ||
} | ||
action, err := provisioning.NewAction(config) | ||
return action, trace.Wrap(err) | ||
} | ||
|
||
type identityProviderDetails struct { | ||
IssuerURL string `json:"issuer_url"` | ||
ClientIDs []string `json:"client_id_list"` | ||
Thumbprints []string `json:"thumbprint_list"` | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,237 @@ | ||
/* | ||
* 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" | ||
"log/slog" | ||
|
||
"github.com/aws/aws-sdk-go-v2/aws" | ||
"github.com/aws/aws-sdk-go-v2/service/iam" | ||
iamtypes "github.com/aws/aws-sdk-go-v2/service/iam/types" | ||
"github.com/gravitational/trace" | ||
|
||
awslib "github.com/gravitational/teleport/lib/cloud/aws" | ||
"github.com/gravitational/teleport/lib/cloud/provisioning" | ||
"github.com/gravitational/teleport/lib/integrations/awsoidc/tags" | ||
) | ||
|
||
// RoleCreator can create an IAM role. | ||
type RoleCreator interface { | ||
// CreateRole creates a new IAM Role. | ||
CreateRole(ctx context.Context, params *iam.CreateRoleInput, optFns ...func(*iam.Options)) (*iam.CreateRoleOutput, error) | ||
} | ||
|
||
// RoleGetter can get an IAM role. | ||
type RoleGetter interface { | ||
// GetRole retrieves information about the specified role, including the | ||
// role's path, GUID, ARN, and the role's trust policy that grants | ||
// permission to assume the role. | ||
GetRole(ctx context.Context, params *iam.GetRoleInput, optFns ...func(*iam.Options)) (*iam.GetRoleOutput, error) | ||
} | ||
|
||
// AssumeRolePolicyUpdater can update an IAM role's trust policy. | ||
type AssumeRolePolicyUpdater interface { | ||
// UpdateAssumeRolePolicy updates the policy that grants an IAM entity | ||
// permission to assume a role. | ||
// This is typically referred to as the "role trust policy". | ||
UpdateAssumeRolePolicy(ctx context.Context, params *iam.UpdateAssumeRolePolicyInput, optFns ...func(*iam.Options)) (*iam.UpdateAssumeRolePolicyOutput, error) | ||
} | ||
|
||
// RoleTagger can tag an AWS IAM role. | ||
type RoleTagger interface { | ||
// TagRole adds one or more tags to an IAM role. The role can be a regular | ||
// role or a service-linked role. If a tag with the same key name already | ||
// exists, then that tag is overwritten with the new value. | ||
TagRole(ctx context.Context, params *iam.TagRoleInput, optFns ...func(*iam.Options)) (*iam.TagRoleOutput, error) | ||
} | ||
|
||
// CreateRole returns a [provisioning.Action] that creates or updates an IAM | ||
// role when invoked. | ||
func CreateRole( | ||
clt interface { | ||
AssumeRolePolicyUpdater | ||
RoleCreator | ||
RoleGetter | ||
RoleTagger | ||
}, | ||
roleName string, | ||
description string, | ||
trustPolicy *awslib.PolicyDocument, | ||
tags tags.AWSTags, | ||
) (*provisioning.Action, error) { | ||
details, err := formatDetails(createRoleDetails{ | ||
Name: roleName, | ||
Description: description, | ||
TrustPolicy: *trustPolicy, | ||
Tags: tags, | ||
}) | ||
if err != nil { | ||
return nil, trace.Wrap(err) | ||
} | ||
|
||
config := provisioning.ActionConfig{ | ||
Name: "CreateRole", | ||
Summary: fmt.Sprintf("Create IAM role %q with a custom trust policy", roleName), | ||
Details: details, | ||
RunnerFn: func(ctx context.Context) error { | ||
slog.InfoContext(ctx, "Checking for existing IAM role", | ||
"role", roleName, | ||
) | ||
getRoleOut, err := clt.GetRole(ctx, &iam.GetRoleInput{ | ||
RoleName: &roleName, | ||
}) | ||
if err != nil { | ||
convertedErr := awslib.ConvertIAMv2Error(err) | ||
if !trace.IsNotFound(convertedErr) { | ||
return trace.Wrap(convertedErr) | ||
} | ||
slog.InfoContext(ctx, "Creating IAM role", "role", roleName) | ||
return trace.Wrap(createRole(ctx, clt, roleName, description, trustPolicy, tags)) | ||
} | ||
|
||
slog.InfoContext(ctx, "IAM role already exists", | ||
"role", roleName, | ||
) | ||
existingTrustPolicy, err := awslib.ParsePolicyDocument(aws.ToString(getRoleOut.Role.AssumeRolePolicyDocument)) | ||
if err != nil { | ||
return trace.Wrap(err) | ||
} | ||
err = ensureTrustPolicy(ctx, clt, roleName, trustPolicy, existingTrustPolicy) | ||
if err != nil { | ||
return trace.Wrap(err) | ||
} | ||
|
||
err = ensureTags(ctx, clt, roleName, tags, getRoleOut.Role.Tags) | ||
if err != nil { | ||
// Tagging an existing role after we update it is a | ||
// nice-to-have, but not a need-to-have. | ||
slog.WarnContext(ctx, "Failed to update IAM role tags", | ||
"role", roleName, | ||
"error", err, | ||
"tags", tags, | ||
) | ||
} | ||
return nil | ||
}, | ||
} | ||
action, err := provisioning.NewAction(config) | ||
return action, trace.Wrap(err) | ||
} | ||
|
||
func createRole( | ||
ctx context.Context, | ||
clt RoleCreator, | ||
roleName string, description string, | ||
trustPolicy *awslib.PolicyDocument, | ||
tags tags.AWSTags, | ||
) error { | ||
trustPolicyJSON, err := trustPolicy.Marshal() | ||
if err != nil { | ||
return trace.Wrap(err) | ||
} | ||
_, err = clt.CreateRole(ctx, &iam.CreateRoleInput{ | ||
RoleName: &roleName, | ||
Description: &description, | ||
AssumeRolePolicyDocument: &trustPolicyJSON, | ||
Tags: tags.ToIAMTags(), | ||
}) | ||
if err != nil { | ||
return trace.Wrap(awslib.ConvertIAMv2Error(err)) | ||
} | ||
slog.InfoContext(ctx, "IAM role created", "role", roleName) | ||
return nil | ||
} | ||
|
||
func ensureTrustPolicy( | ||
ctx context.Context, | ||
clt AssumeRolePolicyUpdater, | ||
roleName string, | ||
trustPolicy *awslib.PolicyDocument, | ||
existingTrustPolicy *awslib.PolicyDocument, | ||
) error { | ||
slog.InfoContext(ctx, "Checking IAM role trust policy", | ||
"role", roleName, | ||
) | ||
|
||
var needsUpdate bool | ||
Outer: | ||
for _, statement := range trustPolicy.Statements { | ||
for _, existingStatement := range existingTrustPolicy.Statements { | ||
if existingStatement.EqualStatement(statement) { | ||
continue Outer | ||
} | ||
} | ||
needsUpdate = true | ||
existingTrustPolicy.Statements = append(existingTrustPolicy.Statements, statement) | ||
} | ||
if !needsUpdate { | ||
slog.InfoContext(ctx, "IAM role trust policy does not require update", | ||
"role", roleName, | ||
) | ||
return nil | ||
} | ||
|
||
slog.InfoContext(ctx, "Updating IAM role trust policy", | ||
"role", roleName, | ||
) | ||
trustPolicyJSON, err := existingTrustPolicy.Marshal() | ||
if err != nil { | ||
return trace.Wrap(err) | ||
} | ||
|
||
_, err = clt.UpdateAssumeRolePolicy(ctx, &iam.UpdateAssumeRolePolicyInput{ | ||
RoleName: &roleName, | ||
PolicyDocument: &trustPolicyJSON, | ||
}) | ||
return trace.Wrap(err) | ||
} | ||
|
||
func ensureTags( | ||
ctx context.Context, | ||
clt RoleTagger, | ||
roleName string, | ||
tags tags.AWSTags, | ||
existingTags []iamtypes.Tag, | ||
) error { | ||
slog.InfoContext(ctx, "Checking for tags on IAM role", | ||
"role", roleName, | ||
"tags", tags, | ||
) | ||
if tags.MatchesIAMTags(existingTags) { | ||
slog.InfoContext(ctx, "IAM role tags already present", | ||
"role", roleName, | ||
) | ||
return nil | ||
} | ||
|
||
_, err := clt.TagRole(ctx, &iam.TagRoleInput{ | ||
RoleName: &roleName, | ||
Tags: tags.ToIAMTags(), | ||
}) | ||
return trace.Wrap(err) | ||
} | ||
|
||
type createRoleDetails struct { | ||
Name string `json:"name"` | ||
Description string `json:"description"` | ||
TrustPolicy awslib.PolicyDocument `json:"trust_policy"` | ||
Tags map[string]string `json:"tags"` | ||
} |
Oops, something went wrong.