package modules import ( "encoding/json" "fmt" "net/http" "os" "strings" "time" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/spf13/cobra" "github.com/spf13/viper" ) var generatePresignedURLCmd = &cobra.Command{ Use: "generate-presigned-url", Short: "Generate presigned url using AWS credentials", Long: `Generate presigned url using AWS credentials.Credentials must be placed in ~/.aws/credentials. You provide profile to load using --profile flag or explicitly provide credentials and region using --aws-access-key-id, --aws-secret-access-key, --region. Note to override credentials you must provide both access key and secret key.`, Example: `frostfs-s3-authmate generate-presigned-url --method put --bucket my-bucket --object my-object --endpoint http://localhost:8084 --lifetime 12h --region ru --aws-access-key-id ETaA2CadPcA7bAkLsML2PbTudXY8uRt2PDjCCwkvRv9s0FDCxWDXYc1SA1vKv8KbyCNsLY2AmAjJ92Vz5rgvsFCy --aws-secret-access-key c2d65ef2980f03f4f495bdebedeeae760496697880d61d106bb9a4e5cd2e0607 --header 'Content-Type: text/plain'`, RunE: runGeneratePresignedURLCmd, } const defaultPresignedLifetime = 12 * time.Hour const ( endpointFlag = "endpoint" bucketFlag = "bucket" objectFlag = "object" methodFlag = "method" profileFlag = "profile" regionFlag = "region" awsAccessKeyIDFlag = "aws-access-key-id" awsSecretAccessKeyFlag = "aws-secret-access-key" headerFlag = "header" sigV4AFlag = "sigv4a" ) func initGeneratePresignedURLCmd() { generatePresignedURLCmd.Flags().Duration(lifetimeFlag, defaultPresignedLifetime, "Lifetime of presigned URL. For example 50h30m (note: max time unit is an hour so to set a day you should use 24h).\nIt will be ceil rounded to the nearest amount of epoch.") generatePresignedURLCmd.Flags().String(endpointFlag, "", "S3 gateway endpoint") generatePresignedURLCmd.Flags().String(bucketFlag, "", "Bucket name to perform action") generatePresignedURLCmd.Flags().String(objectFlag, "", "Object name to perform action") generatePresignedURLCmd.Flags().String(methodFlag, "", "HTTP method to perform action") generatePresignedURLCmd.Flags().String(profileFlag, "", "AWS profile to load") generatePresignedURLCmd.Flags().String(regionFlag, "", "AWS region to use in signature (default is taken from ~/.aws/config)") generatePresignedURLCmd.Flags().String(awsAccessKeyIDFlag, "", "AWS access key id to sign the URL (default is taken from ~/.aws/credentials)") generatePresignedURLCmd.Flags().String(awsSecretAccessKeyFlag, "", "AWS secret access key to sign the URL (default is taken from ~/.aws/credentials)") generatePresignedURLCmd.Flags().StringSlice(headerFlag, nil, "Header in form of 'Key: value' to use in presigned URL (use flags repeatedly for multiple headers or separate them by comma)") generatePresignedURLCmd.Flags().Bool(sigV4AFlag, false, "Use SigV4A for signing request") _ = generatePresignedURLCmd.MarkFlagRequired(endpointFlag) _ = generatePresignedURLCmd.MarkFlagRequired(bucketFlag) _ = generatePresignedURLCmd.MarkFlagRequired(objectFlag) } func runGeneratePresignedURLCmd(cmd *cobra.Command, _ []string) error { var ( region string creds aws.Credentials ) profile := viper.GetString(profileFlag) if profile == "" { cfg, err := config.LoadDefaultConfig(cmd.Context()) if err != nil { return wrapPreparationError(err) } region = cfg.Region if creds, err = cfg.Credentials.Retrieve(cmd.Context()); err != nil { return wrapPreparationError(fmt.Errorf("couldn't get default aws credentials: %w", err)) } } else { cfg, err := config.LoadSharedConfigProfile(cmd.Context(), viper.GetString(profileFlag)) if err != nil { return wrapPreparationError(fmt.Errorf("couldn't get '%s' aws credentials: %w", viper.GetString(profileFlag), err)) } region = cfg.Region creds = cfg.Credentials } accessKeyIDArg := viper.GetString(awsAccessKeyIDFlag) secretAccessKeyArg := viper.GetString(awsSecretAccessKeyFlag) if accessKeyIDArg != "" && secretAccessKeyArg != "" { creds.AccessKeyID = accessKeyIDArg creds.SecretAccessKey = secretAccessKeyArg } if regionArg := viper.GetString(regionFlag); regionArg != "" { region = regionArg } reqData := auth.RequestData{ Method: viper.GetString(methodFlag), Endpoint: viper.GetString(endpointFlag), Bucket: viper.GetString(bucketFlag), Object: viper.GetString(objectFlag), } presignData := auth.PresignData{ Service: "s3", Region: region, Lifetime: viper.GetDuration(lifetimeFlag), SignTime: time.Now().UTC(), } headers, err := parseHeaders() if err != nil { return wrapPreparationError(fmt.Errorf("failed to parse headers: %s", err)) } presignData.Headers = headers var req *http.Request if viper.GetBool(sigV4AFlag) { req, err = auth.PresignRequestV4a(creds, reqData, presignData) } else { req, err = auth.PresignRequest(cmd.Context(), creds, reqData, presignData) } if err != nil { return wrapBusinessLogicError(err) } res := &struct{ URL string }{ URL: req.URL.String(), } enc := json.NewEncoder(os.Stdout) enc.SetIndent("", " ") enc.SetEscapeHTML(false) err = enc.Encode(res) if err != nil { return wrapBusinessLogicError(err) } return nil } func parseHeaders() (map[string]string, error) { headers := viper.GetStringSlice(headerFlag) if len(headers) == 0 { return nil, nil } result := make(map[string]string, len(headers)) for _, header := range headers { k, v, found := strings.Cut(header, ":") if !found { return nil, fmt.Errorf("invalid header format: %s", header) } result[strings.Trim(k, " ")] = strings.Trim(v, " ") } return result, nil }