Skip to content

Commit

Permalink
make configure awsoidc-idp actions transparent
Browse files Browse the repository at this point in the history
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
GavinFrazar committed Sep 24, 2024
1 parent b62dfd8 commit da5197f
Show file tree
Hide file tree
Showing 10 changed files with 1,135 additions and 108 deletions.
32 changes: 32 additions & 0 deletions lib/cloud/provisioning/awsactions/common.go
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)
}
94 changes: 94 additions & 0 deletions lib/cloud/provisioning/awsactions/create_oidc_idp.go
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"`
}
237 changes: 237 additions & 0 deletions lib/cloud/provisioning/awsactions/create_role.go
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"`
}
Loading

0 comments on commit da5197f

Please sign in to comment.