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