diff --git a/api/auth/presign.go b/api/auth/presign.go index ea34ba1..df66d50 100644 --- a/api/auth/presign.go +++ b/api/auth/presign.go @@ -23,6 +23,7 @@ type PresignData struct { Region string Lifetime time.Duration SignTime time.Time + Headers map[string]string } // PresignRequest forms pre-signed request to access objects without aws credentials. @@ -34,7 +35,10 @@ func PresignRequest(creds *credentials.Credentials, reqData RequestData, presign } req.Header.Set(AmzDate, presignData.SignTime.Format("20060102T150405Z")) - req.Header.Set(ContentTypeHdr, "text/plain") + + for k, v := range presignData.Headers { + req.Header.Set(k, v) + } signer := v4.NewSigner(creds) signer.DisableURIPathEscaping = true diff --git a/cmd/s3-authmate/modules/generate-presigned-url.go b/cmd/s3-authmate/modules/generate-presigned-url.go index d25a01c..764187c 100644 --- a/cmd/s3-authmate/modules/generate-presigned-url.go +++ b/cmd/s3-authmate/modules/generate-presigned-url.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "os" + "strings" "time" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth" @@ -21,7 +22,7 @@ var generatePresignedURLCmd = &cobra.Command{ 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`, + 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, } @@ -36,6 +37,7 @@ const ( regionFlag = "region" awsAccessKeyIDFlag = "aws-access-key-id" awsSecretAccessKeyFlag = "aws-secret-access-key" + headerFlag = "header" ) func initGeneratePresignedURLCmd() { @@ -48,6 +50,7 @@ func initGeneratePresignedURLCmd() { 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.MarkFlagRequired(endpointFlag) _ = generatePresignedURLCmd.MarkFlagRequired(bucketFlag) @@ -92,6 +95,12 @@ func runGeneratePresignedURLCmd(*cobra.Command, []string) error { SignTime: time.Now().UTC(), } + headers, err := parseHeaders() + if err != nil { + return wrapPreparationError(fmt.Errorf("failed to parse headers: %s", err)) + } + presignData.Headers = headers + req, err := auth.PresignRequest(sess.Config.Credentials, reqData, presignData) if err != nil { return wrapBusinessLogicError(err) @@ -110,3 +119,21 @@ func runGeneratePresignedURLCmd(*cobra.Command, []string) error { } 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 +} diff --git a/docs/authmate.md b/docs/authmate.md index 5c947ee..2cee910 100644 --- a/docs/authmate.md +++ b/docs/authmate.md @@ -349,6 +349,25 @@ $ frostfs-s3-authmate generate-presigned-url --endpoint http://localhost:8084 \ } ``` +### Upload file with presigned URL + +1. Generate presigned URL to upload object `obj` to bucket `presigned` + + ```shell + $ frostfs-s3-authmate generate-presigned-url --endpoint http://localhost:8084 \ + --method put --bucket presigned --object obj --lifetime 30s + + { + "URL": "http://localhost:8084/presigned/obj?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=CYfbvKwSC9VNvttj5snyEZ5Ttr2VaBabpw7mRuEzNXyw09ewUERj6MGDKfyckfg5VZ39GfXbwLwz62UPVeRxhJDet%2F20241029%2Fdefault%2Fs3%2Faws4_request&X-Amz-Date=20241029T145726Z&X-Amz-Expires=30&X-Amz-SignedHeaders=host&X-Amz-Signature=2bb13b3e6448968219ad95147debe49e37bce5ce3ed1344c4015f43cb444a956" + } + ``` + +2. Upload file using `curl` + + ```shell + curl --upload-file /path/to/file 'http://localhost:8084/presigned/obj?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=CYfbvKwSC9VNvttj5snyEZ5Ttr2VaBabpw7mRuEzNXyw09ewUERj6MGDKfyckfg5VZ39GfXbwLwz62UPVeRxhJDet%2F20241029%2Fdefault%2Fs3%2Faws4_request&X-Amz-Date=20241029T145726Z&X-Amz-Expires=30&X-Amz-SignedHeaders=host&X-Amz-Signature=2bb13b3e6448968219ad95147debe49e37bce5ce3ed1344c4015f43cb444a956' + ``` + ### AWS CLI You can also can get the presigned URL (only for GET) using aws cli v2: