Skip to content

Commit

Permalink
make configure awsoidc-idp actions transparent (#46747)
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 authored Oct 3, 2024
1 parent e6051f7 commit a3e1316
Show file tree
Hide file tree
Showing 17 changed files with 1,294 additions and 196 deletions.
164 changes: 111 additions & 53 deletions lib/cloud/aws/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,25 +84,71 @@ type Statement struct {
// Condition:
// StringEquals:
// "proxy.example.com:aud": "discover.teleport"
Conditions map[string]map[string]SliceOrString `json:"Condition,omitempty"`
Conditions Conditions `json:"Condition,omitempty"`
// StatementID is an optional identifier for the statement.
StatementID string `json:"Sid,omitempty"`
}

// Conditions is a list of conditions that must be satisfied for an action to be allowed.
type Conditions map[string]StringOrMap

// Equals returns true if conditions are equal.
func (a Conditions) Equals(b Conditions) bool {
if len(a) != len(b) {
return false
}
for conditionKindA, conditionOpA := range a {
conditionOpB := b[conditionKindA]
if !conditionOpA.Equals(conditionOpB) {
return false
}
}
return true
}

// ensureResource ensures that the statement contains the specified resource.
//
// Returns true if the resource was already a part of the statement.
// Returns true if the resource was added to the statement or false if the
// resource was already part of the statement.
func (s *Statement) ensureResource(resource string) bool {
if slices.Contains(s.Resources, resource) {
return true
return false
}
s.Resources = append(s.Resources, resource)
return false
return true
}
func (s *Statement) ensureResources(resources []string) {
func (s *Statement) ensureResources(resources []string) bool {
var updated bool
for _, resource := range resources {
s.ensureResource(resource)
updated = s.ensureResource(resource) || updated
}
return updated
}

// ensurePrincipal ensures that the statement contains the specified principal.
//
// Returns true if the principal was already a part of the statement.
func (s *Statement) ensurePrincipal(kind string, value string) bool {
if len(s.Principals) == 0 {
s.Principals = make(StringOrMap)
}
values := s.Principals[kind]
if slices.Contains(values, value) {
return false
}
values = append(values, value)
s.Principals[kind] = values
return true
}

func (s *Statement) ensurePrincipals(principals StringOrMap) bool {
var updated bool
for kind, values := range principals {
for _, v := range values {
updated = s.ensurePrincipal(kind, v) || updated
}
}
return updated
}

// EqualStatement returns whether the receive statement is the same.
Expand All @@ -115,39 +161,17 @@ func (s *Statement) EqualStatement(other *Statement) bool {
return false
}

if len(s.Principals) != len(other.Principals) {
if !s.Principals.Equals(other.Principals) {
return false
}

for principalKind, principalList := range s.Principals {
expectedPrincipalList := other.Principals[principalKind]
if !slices.Equal(principalList, expectedPrincipalList) {
return false
}
}

if !slices.Equal(s.Resources, other.Resources) {
return false
}

if len(s.Conditions) != len(other.Conditions) {
if !s.Conditions.Equals(other.Conditions) {
return false
}
for conditionKind, conditionOp := range s.Conditions {
expectedConditionOp := other.Conditions[conditionKind]

if len(conditionOp) != len(expectedConditionOp) {
return false
}

for conditionOpKind, conditionOpList := range conditionOp {
expectedConditionOpList := expectedConditionOp[conditionOpKind]
if !slices.Equal(conditionOpList, expectedConditionOpList) {
return false
}
}
}

return true
}

Expand All @@ -174,33 +198,38 @@ func NewPolicyDocument(statements ...*Statement) *PolicyDocument {
}
}

// Ensure ensures that the policy document contains the specified resource
// action.
// EnsureResourceAction ensures that the policy document contains the specified
// resource action.
//
// Returns true if the resource action was already a part of the policy and
// false otherwise.
func (p *PolicyDocument) Ensure(effect, action, resource string) bool {
if existingStatement := p.findStatement(effect, action); existingStatement != nil {
// Returns true if the resource action was added to the policy and false if it
// was already part of the policy.
func (p *PolicyDocument) EnsureResourceAction(effect, action, resource string, conditions Conditions) bool {
if existingStatement := p.findStatement(effect, action, conditions); existingStatement != nil {
return existingStatement.ensureResource(resource)
}

// No statement yet for this resource action, add it.
p.Statements = append(p.Statements, &Statement{
Effect: effect,
Actions: []string{action},
Resources: []string{resource},
Effect: effect,
Actions: []string{action},
Resources: []string{resource},
Conditions: conditions,
})
return false
return true
}

// Delete deletes the specified resource action from the policy.
func (p *PolicyDocument) Delete(effect, action, resource string) {
func (p *PolicyDocument) DeleteResourceAction(effect, action, resource string, conditions Conditions) {
var statements []*Statement
for _, s := range p.Statements {
if s.Effect != effect {
statements = append(statements, s)
continue
}
if !s.Conditions.Equals(conditions) {
statements = append(statements, s)
continue
}
var resources []string
for _, a := range s.Actions {
for _, r := range s.Resources {
Expand All @@ -225,7 +254,8 @@ func (p *PolicyDocument) Delete(effect, action, resource string) {
//
// The main benefit of using this function (versus appending to p.Statements
// directly) is to avoid duplications.
func (p *PolicyDocument) EnsureStatements(statements ...*Statement) {
func (p *PolicyDocument) EnsureStatements(statements ...*Statement) bool {
var updated bool
for _, statement := range statements {
if statement == nil {
continue
Expand All @@ -234,8 +264,9 @@ func (p *PolicyDocument) EnsureStatements(statements ...*Statement) {
// Try to find an existing statement by the action, and add the resources there.
var newActions []string
for _, action := range statement.Actions {
if existingStatement := p.findStatement(statement.Effect, action); existingStatement != nil {
existingStatement.ensureResources(statement.Resources)
if existingStatement := p.findStatement(statement.Effect, action, statement.Conditions); existingStatement != nil {
updated = existingStatement.ensureResources(statement.Resources) || updated
updated = existingStatement.ensurePrincipals(statement.Principals) || updated
} else {
newActions = append(newActions, action)
}
Expand All @@ -244,12 +275,21 @@ func (p *PolicyDocument) EnsureStatements(statements ...*Statement) {
// Add the leftover actions as a new statement.
if len(newActions) > 0 {
p.Statements = append(p.Statements, &Statement{
Effect: statement.Effect,
Actions: newActions,
Resources: statement.Resources,
Effect: statement.Effect,
Actions: newActions,
Resources: statement.Resources,
Conditions: statement.Conditions,
Principals: statement.Principals,
})
updated = true
}
}
return updated
}

// IsEmpty returns whether the policy document is empty.
func (p *PolicyDocument) IsEmpty() bool {
return len(p.Statements) == 0
}

// Marshal formats the PolicyDocument in a "friendly" format, which can be
Expand All @@ -264,27 +304,30 @@ func (p *PolicyDocument) Marshal() (string, error) {
}

// ForEach loops through each action and resource of each statement.
func (p *PolicyDocument) ForEach(fn func(effect, action, resource string)) {
func (p *PolicyDocument) ForEach(fn func(effect, action, resource string, conditions Conditions)) {
for _, statement := range p.Statements {
for _, action := range statement.Actions {
for _, resource := range statement.Resources {
fn(statement.Effect, action, resource)
fn(statement.Effect, action, resource, statement.Conditions)
}
}
}
}

func (p *PolicyDocument) findStatement(effect, action string) *Statement {
func (p *PolicyDocument) findStatement(effect, action string, conditions Conditions) *Statement {
for _, s := range p.Statements {
if s.Effect != effect {
continue
}
if slices.Contains(s.Actions, action) {
return s
if !slices.Contains(s.Actions, action) {
continue
}
if !s.Conditions.Equals(conditions) {
continue
}
return s
}
return nil

}

// SliceOrString defines a type that can be either a single string or a slice.
Expand Down Expand Up @@ -337,6 +380,21 @@ func (s SliceOrString) MarshalJSON() ([]byte, error) {
// https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_principal.html#principal-anonymous
type StringOrMap map[string]SliceOrString

// Equals returns true if this StringOrMap is equal to another StringOrMap.
func (s StringOrMap) Equals(other StringOrMap) bool {
if len(s) != len(other) {
return false
}

for key, list := range s {
otherList := other[key]
if !slices.Equal(list, otherList) {
return false
}
}
return true
}

// UnmarshalJSON implements json.Unmarshaller.
// If it contains a string and not a map, it will create a map with a single entry:
// { "str": [] }
Expand Down
4 changes: 2 additions & 2 deletions lib/cloud/aws/policy_statements.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ func StatementForAWSAppAccess() *Statement {
"sts:AssumeRole",
},
Resources: allResources,
Conditions: map[string]map[string]SliceOrString{
Conditions: map[string]StringOrMap{
"StringEquals": {
"iam:ResourceTag/" + requiredTag: SliceOrString{"true"},
},
Expand Down Expand Up @@ -198,7 +198,7 @@ func StatementForAWSOIDCRoleTrustRelationship(accountID, providerURL string, aud
Principals: map[string]SliceOrString{
"Federated": []string{federatedARN},
},
Conditions: map[string]map[string]SliceOrString{
Conditions: map[string]StringOrMap{
"StringEquals": {
federatedAudience: audiences,
},
Expand Down
Loading

0 comments on commit a3e1316

Please sign in to comment.