1// Copyright (C) 2020 Storj Labs, Inc.
2// See LICENSE for copying information.
3
4package cmd
5
6import (
7	"bytes"
8	"context"
9	"encoding/base64"
10	"encoding/json"
11	"fmt"
12	"io/ioutil"
13	"net/http"
14	"time"
15
16	"github.com/btcsuite/btcutil/base58"
17	"github.com/spf13/cobra"
18	"github.com/zeebo/errs"
19
20	"storj.io/common/macaroon"
21	"storj.io/common/pb"
22	"storj.io/private/cfgstruct"
23	"storj.io/private/process"
24	"storj.io/uplink"
25)
26
27const defaultAccessRegisterTimeout = 15 * time.Second
28
29type registerConfig struct {
30	AuthService string `help:"the address to the service you wish to register your access with" default:"" basic-help:"true"`
31	Public      bool   `help:"if the access should be public" default:"false" basic-help:"true"`
32	Format      string `help:"format of credentials, use 'env' or 'aws' for using in scripts" default:""`
33	AWSProfile  string `help:"if using --format=aws, output the --profile tag using this profile" default:""`
34	AccessConfig
35}
36
37var (
38	inspectCfg  AccessConfig
39	listCfg     AccessConfig
40	registerCfg registerConfig
41)
42
43func init() {
44	// We skip the use of addCmd here because we only want the configuration options listed
45	// above, and addCmd adds a whole lot more than we want.
46	accessCmd := &cobra.Command{
47		Use:   "access",
48		Short: "Set of commands to manage access.",
49	}
50
51	inspectCmd := &cobra.Command{
52		Use:   "inspect [ACCESS]",
53		Short: "Inspect allows you to explode a serialized access into its constituent parts.",
54		RunE:  accessInspect,
55		Args:  cobra.MaximumNArgs(1),
56	}
57
58	listCmd := &cobra.Command{
59		Use:   "list",
60		Short: "Prints name and associated satellite of all available accesses.",
61		RunE:  accessList,
62		Args:  cobra.NoArgs,
63	}
64
65	registerCmd := &cobra.Command{
66		Use:   "register [ACCESS]",
67		Short: "Register your access for use with a hosted gateway.",
68		RunE:  accessRegister,
69		Args:  cobra.MaximumNArgs(1),
70	}
71
72	RootCmd.AddCommand(accessCmd)
73	accessCmd.AddCommand(inspectCmd)
74	accessCmd.AddCommand(listCmd)
75	accessCmd.AddCommand(registerCmd)
76
77	process.Bind(inspectCmd, &inspectCfg, defaults, cfgstruct.ConfDir(getConfDir()))
78	process.Bind(listCmd, &listCfg, defaults, cfgstruct.ConfDir(getConfDir()))
79	process.Bind(registerCmd, &registerCfg, defaults, cfgstruct.ConfDir(getConfDir()))
80}
81
82func accessList(cmd *cobra.Command, args []string) (err error) {
83	accesses := listCfg.Accesses
84	fmt.Println("=========== ACCESSES LIST: name / satellite ================================")
85	for name, data := range accesses {
86		satelliteAddr, _, _, err := parseAccess(data)
87		if err != nil {
88			return err
89		}
90
91		fmt.Println(name, "/", satelliteAddr)
92	}
93	return nil
94}
95
96type base64url []byte
97
98func (b base64url) MarshalJSON() ([]byte, error) {
99	return []byte(`"` + base64.URLEncoding.EncodeToString(b) + `"`), nil
100}
101
102type accessInfo struct {
103	SatelliteAddr    string               `json:"satellite_addr"`
104	EncryptionAccess *pb.EncryptionAccess `json:"encryption_access"`
105	APIKey           string               `json:"api_key"`
106	Macaroon         accessInfoMacaroon   `json:"macaroon"`
107}
108
109type accessInfoMacaroon struct {
110	Head    base64url         `json:"head"`
111	Caveats []macaroon.Caveat `json:"caveats"`
112	Tail    base64url         `json:"tail"`
113}
114
115func accessInspect(cmd *cobra.Command, args []string) (err error) {
116	// FIXME: This is inefficient. We end up parsing, serializing, parsing
117	// again. It can get particularly bad with large access grants.
118	access, err := getAccessFromArgZeroOrConfig(inspectCfg, args)
119	if err != nil {
120		return errs.New("no access specified: %w", err)
121	}
122
123	serializedAccess, err := access.Serialize()
124	if err != nil {
125		return err
126	}
127
128	p, err := parseAccessRaw(serializedAccess)
129	if err != nil {
130		return err
131	}
132
133	m, err := macaroon.ParseMacaroon(p.ApiKey)
134	if err != nil {
135		return err
136	}
137
138	// TODO: this could be better
139	apiKey, err := macaroon.ParseRawAPIKey(p.ApiKey)
140	if err != nil {
141		return err
142	}
143
144	ai := accessInfo{
145		SatelliteAddr:    p.SatelliteAddr,
146		EncryptionAccess: p.EncryptionAccess,
147		APIKey:           apiKey.Serialize(),
148		Macaroon: accessInfoMacaroon{
149			Head:    m.Head(),
150			Caveats: []macaroon.Caveat{},
151			Tail:    m.Tail(),
152		},
153	}
154
155	for _, cb := range m.Caveats() {
156		var c macaroon.Caveat
157
158		err := pb.Unmarshal(cb, &c)
159		if err != nil {
160			return err
161		}
162
163		ai.Macaroon.Caveats = append(ai.Macaroon.Caveats, c)
164	}
165
166	bs, err := json.MarshalIndent(ai, "", "  ")
167	if err != nil {
168		return err
169	}
170
171	fmt.Println(string(bs))
172
173	return nil
174}
175
176func parseAccessRaw(access string) (_ *pb.Scope, err error) {
177	data, version, err := base58.CheckDecode(access)
178	if err != nil || version != 0 {
179		return nil, errs.New("invalid access grant format: %w", err)
180	}
181
182	p := new(pb.Scope)
183	if err := pb.Unmarshal(data, p); err != nil {
184		return nil, err
185	}
186
187	return p, nil
188}
189
190func parseAccess(access string) (sa string, apiKey string, ea string, err error) {
191	p, err := parseAccessRaw(access)
192	if err != nil {
193		return "", "", "", err
194	}
195
196	eaData, err := pb.Marshal(p.EncryptionAccess)
197	if err != nil {
198		return "", "", "", errs.New("unable to marshal encryption access: %w", err)
199	}
200
201	apiKey = base58.CheckEncode(p.ApiKey, 0)
202	ea = base58.CheckEncode(eaData, 0)
203	return p.SatelliteAddr, apiKey, ea, nil
204}
205
206func accessRegister(cmd *cobra.Command, args []string) (err error) {
207	ctx, _ := withTelemetry(cmd)
208
209	access, err := getAccessFromArgZeroOrConfig(registerCfg.AccessConfig, args)
210	if err != nil {
211		return errs.New("no access specified: %w", err)
212	}
213
214	accessKey, secretKey, endpoint, err := RegisterAccess(ctx, access, registerCfg.AuthService, registerCfg.Public, defaultAccessRegisterTimeout)
215	if err != nil {
216		return err
217	}
218
219	return DisplayGatewayCredentials(accessKey, secretKey, endpoint, registerCfg.Format, registerCfg.AWSProfile)
220}
221
222func getAccessFromArgZeroOrConfig(config AccessConfig, args []string) (access *uplink.Access, err error) {
223	if len(args) != 0 {
224		access, err = config.GetNamedAccess(args[0])
225		if err != nil {
226			return nil, err
227		}
228		if access != nil {
229			return access, nil
230		}
231		return uplink.ParseAccess(args[0])
232	}
233	return config.GetAccess()
234}
235
236// DisplayGatewayCredentials formats and writes credentials to stdout.
237func DisplayGatewayCredentials(accessKey, secretKey, endpoint, format, awsProfile string) (err error) {
238	switch format {
239	case "env": // export / set compatible format
240		// note that AWS_ENDPOINT configuration is not natively utilized by the AWS CLI
241		_, err = fmt.Printf("AWS_ACCESS_KEY_ID=%s\n"+
242			"AWS_SECRET_ACCESS_KEY=%s\n"+
243			"AWS_ENDPOINT=%s\n",
244			accessKey, secretKey, endpoint)
245		if err != nil {
246			return err
247		}
248	case "aws": // aws configuration commands
249		profile := ""
250		if awsProfile != "" {
251			profile = " --profile " + awsProfile
252			_, err = fmt.Printf("aws configure %s\n", profile)
253			if err != nil {
254				return err
255			}
256		}
257		// note that the endpoint_url configuration is not natively utilized by the AWS CLI
258		_, err = fmt.Printf("aws configure %s set aws_access_key_id %s\n"+
259			"aws configure %s set aws_secret_access_key %s\n"+
260			"aws configure %s set s3.endpoint_url %s\n",
261			profile, accessKey, profile, secretKey, profile, endpoint)
262		if err != nil {
263			return err
264		}
265	default: // plain text
266		_, err = fmt.Printf("========== CREDENTIALS ===================================================================\n"+
267			"Access Key ID: %s\n"+
268			"Secret Key   : %s\n"+
269			"Endpoint     : %s\n",
270			accessKey, secretKey, endpoint)
271		if err != nil {
272			return err
273		}
274	}
275	return nil
276}
277
278// RegisterAccess registers an access grant with a Gateway Authorization Service.
279func RegisterAccess(ctx context.Context, access *uplink.Access, authService string, public bool, timeout time.Duration) (accessKey, secretKey, endpoint string, err error) {
280	if authService == "" {
281		return "", "", "", errs.New("no auth service address provided")
282	}
283	accesssSerialized, err := access.Serialize()
284	if err != nil {
285		return "", "", "", errs.Wrap(err)
286	}
287	postData, err := json.Marshal(map[string]interface{}{
288		"access_grant": accesssSerialized,
289		"public":       public,
290	})
291	if err != nil {
292		return accessKey, "", "", errs.Wrap(err)
293	}
294
295	client := &http.Client{
296		Timeout: timeout,
297	}
298
299	req, err := http.NewRequestWithContext(ctx, http.MethodPost, fmt.Sprintf("%s/v1/access", authService), bytes.NewReader(postData))
300	if err != nil {
301		return "", "", "", err
302	}
303	req.Header.Set("Content-Type", "application/json")
304
305	resp, err := client.Do(req)
306	if err != nil {
307		return "", "", "", err
308	}
309	defer func() { err = errs.Combine(err, resp.Body.Close()) }()
310
311	body, err := ioutil.ReadAll(resp.Body)
312	if err != nil {
313		return "", "", "", err
314	}
315
316	respBody := make(map[string]string)
317	if err := json.Unmarshal(body, &respBody); err != nil {
318		return "", "", "", errs.New("unexpected response from auth service: %s", string(body))
319	}
320
321	accessKey, ok := respBody["access_key_id"]
322	if !ok {
323		return "", "", "", errs.New("access_key_id missing in response")
324	}
325	secretKey, ok = respBody["secret_key"]
326	if !ok {
327		return "", "", "", errs.New("secret_key missing in response")
328	}
329	return accessKey, secretKey, respBody["endpoint"], nil
330}
331