1/* Copyright 2017 WALLIX 2 3Licensed under the Apache License, Version 2.0 (the "License"); 4you may not use this file except in compliance with the License. 5You may obtain a copy of the License at 6 7 http://www.apache.org/licenses/LICENSE-2.0 8 9Unless required by applicable law or agreed to in writing, software 10distributed under the License is distributed on an "AS IS" BASIS, 11WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12See the License for the specific language governing permissions and 13limitations under the License. 14*/ 15 16package awsspec 17 18import ( 19 "errors" 20 "fmt" 21 "os" 22 "path/filepath" 23 "regexp" 24 "strings" 25 26 "github.com/wallix/awless/cloud/match" 27 "github.com/wallix/awless/template/env" 28 "github.com/wallix/awless/template/params" 29 30 "github.com/aws/aws-sdk-go/aws" 31 "github.com/aws/aws-sdk-go/aws/credentials" 32 "github.com/aws/aws-sdk-go/service/iam" 33 "github.com/aws/aws-sdk-go/service/iam/iamiface" 34 "github.com/wallix/awless/aws/config" 35 "github.com/wallix/awless/cloud" 36 "github.com/wallix/awless/cloud/properties" 37 "github.com/wallix/awless/logger" 38) 39 40type CreateAccesskey struct { 41 _ string `action:"create" entity:"accesskey" awsAPI:"iam" awsCall:"CreateAccessKey" awsInput:"iam.CreateAccessKeyInput" awsOutput:"iam.CreateAccessKeyOutput"` 42 logger *logger.Logger 43 graph cloud.GraphAPI 44 api iamiface.IAMAPI 45 User *string `awsName:"UserName" awsType:"awsstr" templateName:"user"` 46 Save *bool `templateName:"save"` 47} 48 49func (cmd *CreateAccesskey) ParamsSpec() params.Spec { 50 builder := params.SpecBuilder(params.AllOf(params.Key("user"), 51 params.Opt("save", "no-prompt"), 52 )) 53 builder.AddReducer( 54 func(values map[string]interface{}) (map[string]interface{}, error) { 55 if noPrompt, hasNoPrompt := values["no-prompt"]; hasNoPrompt { 56 b, err := castBool(noPrompt) 57 if err != nil { 58 return nil, fmt.Errorf("no-prompt: %s", err) 59 } 60 return map[string]interface{}{"save": !b}, nil 61 } else { 62 return nil, nil 63 } 64 }, 65 "no-prompt", 66 ) 67 return builder.Done() 68} 69 70func (cmd *CreateAccesskey) AfterRun(renv env.Running, output interface{}) error { 71 accessKey := output.(*iam.CreateAccessKeyOutput).AccessKey 72 if !BoolValue(cmd.Save) { 73 cmd.logger.Infof("Access key created. Here are the crendentials for user %s:", aws.StringValue(accessKey.UserName)) 74 fmt.Fprintln(os.Stderr) 75 fmt.Fprintln(os.Stderr, strings.Repeat("*", 64)) 76 fmt.Fprintf(os.Stderr, "aws_access_key_id = %s\n", aws.StringValue(accessKey.AccessKeyId)) 77 fmt.Fprintf(os.Stderr, "aws_secret_access_key = %s\n", aws.StringValue(accessKey.SecretAccessKey)) 78 fmt.Fprintln(os.Stderr, strings.Repeat("*", 64)) 79 fmt.Fprintln(os.Stderr) 80 cmd.logger.Warning("This is your only opportunity to view the secret access keys.") 81 cmd.logger.Warning("Save the user's new access key ID and secret access key in a safe and secure place.") 82 cmd.logger.Warning("You will not have access to the secret keys again after this step.\n") 83 } 84 85 if cmd.Save != nil && !BoolValue(cmd.Save) { 86 return nil 87 } 88 profile := StringValue(cmd.User) 89 if !BoolValue(cmd.Save) { 90 if !promptConfirm("Do you want to save these access keys in %s?", AWSCredFilepath) { 91 return nil 92 } 93 profile = promptStringWithDefault("Entry profile name: ("+StringValue(cmd.User)+") ", profile) 94 } 95 96 creds := NewCredsPrompter(profile) 97 creds.Val.AccessKeyID = aws.StringValue(accessKey.AccessKeyId) 98 creds.Val.SecretAccessKey = aws.StringValue(accessKey.SecretAccessKey) 99 created, err := creds.Store() 100 if err != nil { 101 logger.Errorf("cannot store access keys: %s", err) 102 } else { 103 if created { 104 fmt.Fprintf(os.Stderr, "\n\u2713 %s created", AWSCredFilepath) 105 } 106 fmt.Fprintf(os.Stderr, "\n\u2713 Credentials for profile '%s' stored successfully in %s\n\n", creds.Profile, AWSCredFilepath) 107 } 108 109 return nil 110} 111 112func (cmd *CreateAccesskey) ExtractResult(i interface{}) string { 113 return StringValue(i.(*iam.CreateAccessKeyOutput).AccessKey.AccessKeyId) 114} 115 116type DeleteAccesskey struct { 117 _ string `action:"delete" entity:"accesskey" awsAPI:"iam" awsCall:"DeleteAccessKey" awsInput:"iam.DeleteAccessKeyInput" awsOutput:"iam.DeleteAccessKeyOutput"` 118 logger *logger.Logger 119 graph cloud.GraphAPI 120 api iamiface.IAMAPI 121 Id *string `awsName:"AccessKeyId" awsType:"awsstr" templateName:"id"` 122 User *string `awsName:"UserName" awsType:"awsstr" templateName:"user"` 123} 124 125func (cmd *DeleteAccesskey) ParamsSpec() params.Spec { 126 builder := params.SpecBuilder(params.AtLeastOneOf(params.Key("id"), params.Key("user"))) 127 builder.AddReducer( 128 func(values map[string]interface{}) (map[string]interface{}, error) { 129 user, hasUser := values["user"].(string) 130 id, hasId := values["id"].(string) 131 if !hasUser && hasId { 132 r, err := cmd.graph.FindOne(cloud.NewQuery(cloud.AccessKey).Match(match.Property(properties.ID, id))) 133 if err != nil || r == nil { 134 return values, nil 135 } 136 if keyUser, ok := r.Property(properties.Username); ok { 137 values["user"] = keyUser 138 } 139 } else if hasUser && !hasId { 140 keys, err := cmd.api.ListAccessKeys(&iam.ListAccessKeysInput{ 141 UserName: String(user), 142 }) 143 if err != nil { 144 return values, fmt.Errorf("can not find access key for %s: %s", user, err) 145 } 146 switch len(keys.AccessKeyMetadata) { 147 case 0: 148 return values, fmt.Errorf("no access key found for %s:", user) 149 case 1: 150 values["id"] = StringValue(keys.AccessKeyMetadata[0].AccessKeyId) 151 default: 152 var keysStr []string 153 for _, k := range keys.AccessKeyMetadata { 154 keysStr = append(keysStr, fmt.Sprintf("%s (created on %s)", StringValue(k.AccessKeyId), aws.TimeValue(k.CreateDate).Format("2006/01/02 15:04:05"))) 155 } 156 return values, fmt.Errorf("multiple access keys found for %s: %s", user, strings.Join(keysStr, ", ")) 157 } 158 159 } 160 return values, nil 161 }, 162 "user", "id", 163 ) 164 return builder.Done() 165} 166 167var ( 168 AWSCredFilepath = filepath.Join(awsconfig.AWSHomeDir(), "credentials") 169) 170 171type credentialsPrompter struct { 172 Profile string 173 Val credentials.Value 174 ProfileSetterCallback func(string) error 175} 176 177func NewCredsPrompter(profile string) *credentialsPrompter { 178 return &credentialsPrompter{Profile: profile, ProfileSetterCallback: func(string) error { return nil }} 179} 180 181func (c *credentialsPrompter) Prompt() error { 182 token := "and choose a profile name" 183 if c.HasProfile() { 184 token = fmt.Sprintf("for profile '%s'", c.Profile) 185 } 186 fmt.Printf("\nPlease enter access keys %s (stored at %s):\n", token, AWSCredFilepath) 187 188 promptUntilNonEmpty("AWS Access Key ID? ", &c.Val.AccessKeyID) 189 promptUntilNonEmpty("AWS Secret Access Key? ", &c.Val.SecretAccessKey) 190 if c.HasProfile() { 191 promptToOverride(fmt.Sprintf("Change your profile name (or just press Enter to keep '%s')? ", c.Profile), &c.Profile) 192 } else { 193 c.Profile = "default" 194 promptToOverride("Choose a profile name (or just press Enter to have AWS 'default')? ", &c.Profile) 195 } 196 197 if c.ProfileSetterCallback != nil { 198 c.ProfileSetterCallback(c.Profile) 199 } 200 201 return nil 202} 203 204var accessKeysRegex = regexp.MustCompile("^[a-zA-Z0-9/+=]{20,60}$") 205 206func (c *credentialsPrompter) Store() (bool, error) { 207 var created bool 208 209 if c.Val.SecretAccessKey == "" { 210 return created, errors.New("given empty secret access key") 211 } 212 if !accessKeysRegex.MatchString(c.Val.SecretAccessKey) { 213 return created, errors.New("given invalid secret access key") 214 } 215 if c.Val.AccessKeyID == "" { 216 return created, errors.New("given empty access key") 217 } 218 if !accessKeysRegex.MatchString(c.Val.AccessKeyID) { 219 return created, errors.New("given invalid access key") 220 } 221 return appendToAwsFile( 222 fmt.Sprintf("\n[%s]\naws_access_key_id = %s\naws_secret_access_key = %s\n", c.Profile, c.Val.AccessKeyID, c.Val.SecretAccessKey), 223 AWSCredFilepath, 224 ) 225} 226 227func appendToAwsFile(content string, awsFilePath string) (bool, error) { 228 var created bool 229 if awsHomeDirMissing() { 230 if err := os.MkdirAll(awsconfig.AWSHomeDir(), 0700); err != nil { 231 return created, fmt.Errorf("creating '%s' : %s", awsconfig.AWSHomeDir(), err) 232 } 233 created = true 234 } 235 236 f, err := os.OpenFile(awsFilePath, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600) 237 if err != nil { 238 return created, fmt.Errorf("appending to '%s': %s", awsFilePath, err) 239 } 240 241 if _, err := fmt.Fprintf(f, content); err != nil { 242 return created, err 243 } 244 245 return created, nil 246} 247 248func promptConfirm(msg string, a ...interface{}) bool { 249 var yesorno string 250 fmt.Fprintf(os.Stderr, "%s [y/N] ", fmt.Sprintf(msg, a...)) 251 fmt.Scanln(&yesorno) 252 if y := strings.TrimSpace(strings.ToLower(yesorno)); y == "y" || y == "yes" { 253 return true 254 } 255 return false 256} 257 258func (c *credentialsPrompter) HasProfile() bool { 259 return strings.TrimSpace(c.Profile) != "" 260} 261 262func promptToOverride(question string, v *string) { 263 fmt.Print(question) 264 var override string 265 fmt.Scanln(&override) 266 if strings.TrimSpace(override) != "" { 267 *v = override 268 return 269 } 270} 271 272func promptUntilNonEmpty(question string, v *string) { 273 ask := func(v *string) bool { 274 fmt.Print(question) 275 _, err := fmt.Scanln(v) 276 if err == nil && strings.TrimSpace(*v) != "" { 277 return false 278 } 279 if err != nil { 280 fmt.Printf("Error: %s. Retry please...\n", err) 281 } 282 return true 283 } 284 for ask(v) { 285 } 286 return 287} 288 289func awsHomeDirMissing() bool { 290 _, err := os.Stat(awsconfig.AWSHomeDir()) 291 return os.IsNotExist(err) 292} 293