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, ®isterCfg, 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