Skip to content

Commit

Permalink
IAM Join Method (backend implementation) (#10085)
Browse files Browse the repository at this point in the history
  • Loading branch information
nklaassen authored Feb 8, 2022
1 parent e8cd8fa commit e00ff42
Show file tree
Hide file tree
Showing 33 changed files with 2,603 additions and 1,121 deletions.
74 changes: 71 additions & 3 deletions api/types/provisioning.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright 2020 Gravitational, Inc.
Copyright 2020-2022 Gravitational, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand All @@ -25,6 +25,20 @@ import (
"github.com/gravitational/trace"
)

// JoinMethod is the method used for new nodes to join the cluster.
type JoinMethod string

const (
JoinMethodUnspecified JoinMethod = ""
// JoinMethodToken is the default join method, nodes join the cluster by
// presenting a secret token.
JoinMethodToken JoinMethod = "token"
// JoinMethodEC2 indicates that the node will join with the EC2 join method.
JoinMethodEC2 JoinMethod = "ec2"
// JoinMethodIAM indicates that the node will join with the IAM join method.
JoinMethodIAM JoinMethod = "iam"
)

// ProvisionToken is a provisioning token
type ProvisionToken interface {
Resource
Expand All @@ -40,6 +54,8 @@ type ProvisionToken interface {
GetAllowRules() []*TokenRule
// GetAWSIIDTTL returns the TTL of EC2 IIDs
GetAWSIIDTTL() Duration
// GetJoinMethod returns joining method that must be used with this token.
GetJoinMethod() JoinMethod
// V1 returns V1 version of the resource
V1() *ProvisionTokenV1
// String returns user friendly representation of the resource
Expand Down Expand Up @@ -98,8 +114,55 @@ func (p *ProvisionTokenV2) CheckAndSetDefaults() error {
return trace.Wrap(err)
}

if p.Spec.AWSIIDTTL == 0 {
p.Spec.AWSIIDTTL = Duration(5 * time.Minute)
hasAllowRules := len(p.Spec.Allow) > 0
if p.Spec.JoinMethod == JoinMethodUnspecified {
// Default to the ec2 join method if any allow rules were specified,
// else default to the token method. These defaults are necessary for
// backwards compatibility.
if hasAllowRules {
p.Spec.JoinMethod = JoinMethodEC2
} else {
p.Spec.JoinMethod = JoinMethodToken
}
}
switch p.Spec.JoinMethod {
case JoinMethodToken:
if hasAllowRules {
return trace.BadParameter("allow rules are not compatible with the %q join method", JoinMethodToken)
}
case JoinMethodEC2:
if !hasAllowRules {
return trace.BadParameter("the %q join method requires defined token allow rules", JoinMethodEC2)
}
for _, allowRule := range p.Spec.Allow {
if allowRule.AWSARN != "" {
return trace.BadParameter(`the %q join method does not support the "aws_arn" parameter`, JoinMethodEC2)
}
if allowRule.AWSAccount == "" && allowRule.AWSRole == "" {
return trace.BadParameter(`allow rule for %q join method must set "aws_account" or "aws_role"`, JoinMethodEC2)
}
}
if p.Spec.AWSIIDTTL == 0 {
// default to 5 minute ttl if unspecified
p.Spec.AWSIIDTTL = Duration(5 * time.Minute)
}
case JoinMethodIAM:
if !hasAllowRules {
return trace.BadParameter("the %q join method requires defined token allow rules", JoinMethodIAM)
}
for _, allowRule := range p.Spec.Allow {
if allowRule.AWSRole != "" {
return trace.BadParameter(`the %q join method does not support the "aws_role" parameter`, JoinMethodIAM)
}
if len(allowRule.AWSRegions) != 0 {
return trace.BadParameter(`the %q join method does not support the "aws_regions" parameter`, JoinMethodIAM)
}
if allowRule.AWSAccount == "" && allowRule.AWSARN == "" {
return trace.BadParameter(`allow rule for %q join method must set "aws_account" or "aws_arn"`, JoinMethodEC2)
}
}
default:
return trace.BadParameter("unknown join method %q", p.Spec.JoinMethod)
}

return nil
Expand Down Expand Up @@ -132,6 +195,11 @@ func (p *ProvisionTokenV2) GetAWSIIDTTL() Duration {
return p.Spec.AWSIIDTTL
}

// GetJoinMethod returns joining method that must be used with this token.
func (p *ProvisionTokenV2) GetJoinMethod() JoinMethod {
return p.Spec.JoinMethod
}

// GetKind returns resource kind
func (p *ProvisionTokenV2) GetKind() string {
return p.Kind
Expand Down
272 changes: 272 additions & 0 deletions api/types/provisioning_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
/*
Copyright 2022 Gravitational, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package types

import (
"testing"
"time"

"github.com/gravitational/trace"
"github.com/stretchr/testify/require"
)

func TestProvisionTokenV2_CheckAndSetDefaults(t *testing.T) {
testcases := []struct {
desc string
token *ProvisionTokenV2
expected *ProvisionTokenV2
expectedErr error
}{
{
desc: "empty",
token: &ProvisionTokenV2{},
expectedErr: &trace.BadParameterError{},
},
{
desc: "missing roles",
token: &ProvisionTokenV2{
Metadata: Metadata{
Name: "test",
},
},
expectedErr: &trace.BadParameterError{},
},
{
desc: "invalid role",
token: &ProvisionTokenV2{
Metadata: Metadata{
Name: "test",
},
Spec: ProvisionTokenSpecV2{
Roles: []SystemRole{RoleNode, "not a role"},
},
},
expectedErr: &trace.BadParameterError{},
},
{
desc: "simple token",
token: &ProvisionTokenV2{
Metadata: Metadata{
Name: "test",
},
Spec: ProvisionTokenSpecV2{
Roles: []SystemRole{RoleNode},
},
},
expected: &ProvisionTokenV2{
Kind: "token",
Version: "v2",
Metadata: Metadata{
Name: "test",
Namespace: "default",
},
Spec: ProvisionTokenSpecV2{
Roles: []SystemRole{RoleNode},
JoinMethod: "token",
},
},
},
{
desc: "implicit ec2 method",
token: &ProvisionTokenV2{
Metadata: Metadata{
Name: "test",
},
Spec: ProvisionTokenSpecV2{
Roles: []SystemRole{RoleNode},
Allow: []*TokenRule{
&TokenRule{
AWSAccount: "1234",
AWSRole: "1234/role",
AWSRegions: []string{"us-west-2"},
},
},
},
},
expected: &ProvisionTokenV2{
Kind: "token",
Version: "v2",
Metadata: Metadata{
Name: "test",
Namespace: "default",
},
Spec: ProvisionTokenSpecV2{
Roles: []SystemRole{RoleNode},
JoinMethod: "ec2",
Allow: []*TokenRule{
&TokenRule{
AWSAccount: "1234",
AWSRole: "1234/role",
AWSRegions: []string{"us-west-2"},
},
},
AWSIIDTTL: Duration(5 * time.Minute),
},
},
},
{
desc: "explicit ec2 method",
token: &ProvisionTokenV2{
Metadata: Metadata{
Name: "test",
},
Spec: ProvisionTokenSpecV2{
Roles: []SystemRole{RoleNode},
JoinMethod: "ec2",
Allow: []*TokenRule{&TokenRule{AWSAccount: "1234"}},
},
},
expected: &ProvisionTokenV2{
Kind: "token",
Version: "v2",
Metadata: Metadata{
Name: "test",
Namespace: "default",
},
Spec: ProvisionTokenSpecV2{
Roles: []SystemRole{RoleNode},
JoinMethod: "ec2",
Allow: []*TokenRule{&TokenRule{AWSAccount: "1234"}},
AWSIIDTTL: Duration(5 * time.Minute),
},
},
},
{
desc: "ec2 method no allow rules",
token: &ProvisionTokenV2{
Metadata: Metadata{
Name: "test",
},
Spec: ProvisionTokenSpecV2{
Roles: []SystemRole{RoleNode},
JoinMethod: "ec2",
},
},
expectedErr: &trace.BadParameterError{},
},
{
desc: "ec2 method with aws_arn",
token: &ProvisionTokenV2{
Metadata: Metadata{
Name: "test",
},
Spec: ProvisionTokenSpecV2{
Roles: []SystemRole{RoleNode},
JoinMethod: "ec2",
Allow: []*TokenRule{
&TokenRule{
AWSAccount: "1234",
AWSARN: "1234",
},
},
},
},
expectedErr: &trace.BadParameterError{},
},
{
desc: "ec2 method empty rule",
token: &ProvisionTokenV2{
Metadata: Metadata{
Name: "test",
},
Spec: ProvisionTokenSpecV2{
Roles: []SystemRole{RoleNode},
JoinMethod: "ec2",
Allow: []*TokenRule{&TokenRule{}},
},
},
expectedErr: &trace.BadParameterError{},
},
{
desc: "iam method",
token: &ProvisionTokenV2{
Metadata: Metadata{
Name: "test",
},
Spec: ProvisionTokenSpecV2{
Roles: []SystemRole{RoleNode},
JoinMethod: "ec2",
Allow: []*TokenRule{&TokenRule{AWSAccount: "1234"}},
},
},
expected: &ProvisionTokenV2{
Kind: "token",
Version: "v2",
Metadata: Metadata{
Name: "test",
Namespace: "default",
},
Spec: ProvisionTokenSpecV2{
Roles: []SystemRole{RoleNode},
JoinMethod: "ec2",
Allow: []*TokenRule{&TokenRule{AWSAccount: "1234"}},
AWSIIDTTL: Duration(5 * time.Minute),
},
},
},
{
desc: "iam method with aws_role",
token: &ProvisionTokenV2{
Metadata: Metadata{
Name: "test",
},
Spec: ProvisionTokenSpecV2{
Roles: []SystemRole{RoleNode},
JoinMethod: "iam",
Allow: []*TokenRule{
&TokenRule{
AWSAccount: "1234",
AWSRole: "1234/role",
},
},
},
},
expectedErr: &trace.BadParameterError{},
},
{
desc: "iam method with aws_regions",
token: &ProvisionTokenV2{
Metadata: Metadata{
Name: "test",
},
Spec: ProvisionTokenSpecV2{
Roles: []SystemRole{RoleNode},
JoinMethod: "iam",
Allow: []*TokenRule{
&TokenRule{
AWSAccount: "1234",
AWSRegions: []string{"us-west-2"},
},
},
},
},
expectedErr: &trace.BadParameterError{},
},
}

for _, tc := range testcases {
t.Run(tc.desc, func(t *testing.T) {
err := tc.token.CheckAndSetDefaults()
if tc.expectedErr != nil {
require.ErrorAs(t, err, &tc.expectedErr)
return
}
require.NoError(t, err)
require.Equal(t, tc.token, tc.expected)
})
}
}
Loading

0 comments on commit e00ff42

Please sign in to comment.