Skip to content

Commit ffae63b

Browse files
authored
Merge pull request #811 from sushanth0910/release-0.6
Adding missing changes form master to release-0.6
2 parents 5efe3c9 + 140ea06 commit ffae63b

File tree

18 files changed

+1054
-72
lines changed

18 files changed

+1054
-72
lines changed

cmd/aws-iam-authenticator/add.go

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ var addUserCmd = &cobra.Command{
4444
Long: "NOTE: this does not currently support the CRD and file backends",
4545
Run: func(cmd *cobra.Command, args []string) {
4646
if userARN == "" || userName == "" || len(groups) == 0 {
47-
fmt.Printf("invalid empty value in userARN %q, username %q, groups %q", userARN, userName, groups)
47+
fmt.Printf("invalid empty value in userARN %q, username %q, groups %q\n", userARN, userName, groups)
4848
os.Exit(1)
4949
}
5050

@@ -75,16 +75,52 @@ var addRoleCmd = &cobra.Command{
7575
Short: "add a role entity to an existing aws-auth configmap, not for CRD/file backends",
7676
Long: "NOTE: this does not currently support the CRD and file backends",
7777
Run: func(cmd *cobra.Command, args []string) {
78-
if roleARN == "" || userName == "" || len(groups) == 0 {
79-
fmt.Printf("invalid empty value in rolearn %q, username %q, groups %q", roleARN, userName, groups)
78+
if (roleARN == "" && ssoRole == nil) || userName == "" || len(groups) == 0 {
79+
fmt.Printf("invalid empty value in rolearn %q, username %q, groups %q\n", roleARN, userName, groups)
8080
os.Exit(1)
8181
}
8282

83-
checkPrompt(fmt.Sprintf("add rolearn %s, username %s, groups %s", roleARN, userName, groups))
83+
var arnOrSSORole string
84+
switch {
85+
case roleARN != "" && ssoRole != nil:
86+
fmt.Printf("only one of --rolearn or --sso can be supplied\n")
87+
os.Exit(1)
88+
case roleARN != "":
89+
arnOrSSORole = "rolearn"
90+
case ssoRole != nil:
91+
arnOrSSORole = "sso"
92+
93+
for _, key := range []string{"permissionSetName", "accountID"} {
94+
if _, ok := ssoRole[key]; !ok {
95+
fmt.Printf("required key '%s' missing from --sso flag\n", key)
96+
os.Exit(1)
97+
}
98+
}
99+
100+
var ssoPartition string
101+
if partition, ok := ssoRole["partition"]; !ok {
102+
ssoPartition = "aws"
103+
} else {
104+
ssoPartition = partition
105+
}
106+
ssoRoleConfig.PermissionSetName = ssoRole["permissionSetName"]
107+
ssoRoleConfig.AccountID = ssoRole["accountID"]
108+
ssoRoleConfig.Partition = ssoPartition
109+
110+
rm := config.RoleMapping{SSO: ssoRoleConfig}
111+
err := rm.Validate()
112+
if err != nil {
113+
fmt.Printf("error validating --sso: %s\n", err)
114+
os.Exit(1)
115+
}
116+
}
117+
118+
checkPrompt(fmt.Sprintf("add %s %s, username %s, groups %s", arnOrSSORole, roleARN, userName, groups))
84119
cli := createClient()
85120

86121
cm, err := cli.AddRole(&config.RoleMapping{
87122
RoleARN: roleARN,
123+
SSO: ssoRoleConfig,
88124
Username: userName,
89125
Groups: groups,
90126
})
@@ -178,6 +214,10 @@ var (
178214
userName string
179215
groups []string
180216
roleARN string
217+
// ssoRole contains the settings for a config.SSOARNMatcher
218+
// it expects the keys "permissionSetName", "accountID", and "partition" (optional)
219+
ssoRole map[string]string
220+
ssoRoleConfig *config.SSOARNMatcher
181221
)
182222

183223
func init() {
@@ -195,6 +235,7 @@ func init() {
195235
addUserCmd.PersistentFlags().StringSliceVar(&groups, "groups", nil, "A new user groups")
196236

197237
addRoleCmd.PersistentFlags().StringVar(&roleARN, "rolearn", "", "A new role ARN")
238+
addRoleCmd.PersistentFlags().StringToStringVar(&ssoRole, "sso", nil, `Settings for a new SSO role. Expects "permissionSetName", "accountID", and "partition" (optional)`)
198239
addRoleCmd.PersistentFlags().StringVar(&userName, "username", "", "A new user name")
199240
addRoleCmd.PersistentFlags().StringSliceVar(&groups, "groups", nil, "A new role groups")
200241
}

cmd/aws-iam-authenticator/root.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,10 @@ func getConfig() (config.Config, error) {
131131
cfg.ReservedPrefixConfig[c.BackendMode] = c
132132
}
133133
}
134+
if featureGates.Enabled(config.SSORoleMatch) {
135+
logrus.Info("SSORoleMatch feature enabled")
136+
config.SSORoleMatchEnabled = true
137+
}
134138
if featureGates.Enabled(config.ConfiguredInitDirectories) {
135139
logrus.Info("ConfiguredInitDirectories feature enabled")
136140
}

docs/sso_role_matcher.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# SSO Role Matcher
2+
3+
Maps configuration for an AWS SSO managed IAM Role to a Kubernetes username and groups.
4+
5+
## Feature state
6+
7+
Alpha
8+
9+
## Use case
10+
11+
Easy and robust configuration for AWS SSO managed roles, which currently have two main issues:
12+
13+
Firstly - confusing configuration. To use an SSO role, a user needs to map the Role ARN of the SSO ROle, minus the path.
14+
15+
For example: given a permission set `MyPermissionSet`, region `us-east-1` and account number `000000000000`; AWS SSO
16+
creates a role: `arn:aws:iam::000000000000:role/aws-reserved/sso.amazonaws.com/us-east-1/AWSReservedSSO_MyPermissionSet_1234567890abcde`.
17+
18+
To match this role, a user would need to create a mapRoles entry like:
19+
```
20+
mapRoles: |
21+
- rolearn: arn:aws:iam::000000000000:role/AWSReservedSSO_MyPermissionSet_1234567890abcde
22+
username: ...
23+
groups: ...
24+
```
25+
26+
Secondly - brittle configuration. If AWS SSO recreates IAM Roles, they receive a different random suffix and all the users of that
27+
role can no longer authenticate to Kubernetes.
28+
29+
## New UX
30+
31+
Users can create a mapRoles entry that will automatically match roles created by AWS SSO without needing to be updated
32+
every time the roles are changed.
33+
34+
Users will now create mapRoles entries like:
35+
```
36+
mapRoles: |
37+
- sso:
38+
permissionSetName: MyPermissionSet
39+
accountID: "000000000000"
40+
username: ...
41+
groups: ...
42+
```
43+
44+
If the user is using the aws-us-govt or aws-cn partitions, they must specify the partition attribute in the `sso` structure.
45+
```
46+
mapRoles: |
47+
- sso:
48+
permissionSetName: MyPermissionSet
49+
accountID: "000000000000"
50+
partition: "aws-us-govt"
51+
username: ...
52+
groups: ...
53+
```
54+
55+
## Implementation
56+
57+
config.RoleMapping will be extended with a nested structure containing the necessary information to construct a canonicalized
58+
Role Arn. The random suffix will not need to be specified and will instead be matched for the user by constructing the
59+
expected ARN and applying a wildcard to the end.
60+
61+
Users are protected from non-AWS SSO created roles as the AWS API prevents roles being manually created with AWSReservedSSO
62+
at the beginning of their names.

pkg/arn/arnlike.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package arn
2+
3+
import (
4+
"fmt"
5+
"regexp"
6+
"strings"
7+
)
8+
9+
const (
10+
arnDelimiter = ":"
11+
arnSectionsExpected = 6
12+
arnPrefix = "arn:"
13+
14+
// zero-indexed
15+
sectionPartition = 1
16+
sectionService = 2
17+
sectionRegion = 3
18+
sectionAccountID = 4
19+
sectionResource = 5
20+
21+
// errors
22+
invalidPrefix = "invalid prefix"
23+
invalidSections = "not enough sections"
24+
)
25+
26+
// ArnLike takes an ARN and returns true if it is matched by the pattern.
27+
// Each component of the ARN is matched individually as per
28+
// https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition_operators.html#Conditions_ARN
29+
func ArnLike(arn, pattern string) (bool, error) {
30+
// "parse" the input arn into sections
31+
arnSections, err := parse(arn)
32+
if err != nil {
33+
return false, fmt.Errorf("Could not parse input arn: %v", err)
34+
}
35+
patternSections, err := parse(pattern)
36+
if err != nil {
37+
return false, fmt.Errorf("Could not parse ArnLike string: %v", err)
38+
}
39+
40+
// Tidy regexp special characters. Escape the ones not used in ArnLike.
41+
// Replace multiple * with .* - we're assuming `\` is not allowed in ARNs
42+
preparePatternSections(patternSections)
43+
44+
for index := range arnSections {
45+
patternGlob, err := regexp.Compile(patternSections[index])
46+
if err != nil {
47+
return false, fmt.Errorf("Could not parse %s: %v", patternSections[index], err)
48+
}
49+
50+
if !patternGlob.MatchString(arnSections[index]) {
51+
return false, nil
52+
}
53+
}
54+
55+
return true, nil
56+
}
57+
58+
// parse is a copy of arn.Parse from the AWS SDK but represents the ARN as []string
59+
func parse(input string) ([]string, error) {
60+
if !strings.HasPrefix(input, arnPrefix) {
61+
return nil, fmt.Errorf(invalidPrefix)
62+
}
63+
arnSections := strings.SplitN(input, arnDelimiter, arnSectionsExpected)
64+
if len(arnSections) != arnSectionsExpected {
65+
return nil, fmt.Errorf(invalidSections)
66+
}
67+
68+
return arnSections, nil
69+
}
70+
71+
// preparePatternSections goes through each section of the arnLike slice and escapes any meta characters, except for
72+
// `*` and `?` which are replaced by `.*` and `.?` respectively. ^ and $ are added as we require an exact match
73+
func preparePatternSections(arnLikeSlice []string) {
74+
for index, section := range arnLikeSlice {
75+
quotedString := quoteMeta(section)
76+
arnLikeSlice[index] = `^` + quotedString + `$`
77+
}
78+
}
79+
80+
// the below is based on regexp.QuoteMeta to escape metacharacters except for `?` and `*`, changing them to `*` and `.*`
81+
82+
// quoteMeta returns a string that escapes all regular expression metacharacters
83+
// inside the argument text; the returned string is a regular expression matching
84+
// the literal text.
85+
func quoteMeta(s string) string {
86+
const specialChars = `\.+()|[]{}^$`
87+
88+
var i int
89+
b := make([]byte, 2*len(s)-i)
90+
copy(b, s[:i])
91+
j := i
92+
for ; i < len(s); i++ {
93+
if strings.Contains(specialChars, s[i:i+1]) {
94+
b[j] = '\\'
95+
j++
96+
} else if s[i] == '*' || s[i] == '?' {
97+
b[j] = '.'
98+
j++
99+
}
100+
b[j] = s[i]
101+
j++
102+
}
103+
return string(b[:j])
104+
}

0 commit comments

Comments
 (0)