2023-06-23 13:11:57 +00:00
package modules
import (
"encoding/json"
"fmt"
2024-04-23 11:49:34 +00:00
"net/http"
2023-06-23 13:11:57 +00:00
"os"
2024-10-29 13:26:01 +00:00
"strings"
2023-06-23 13:11:57 +00:00
"time"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth"
2024-04-23 11:49:34 +00:00
credentialsv2 "github.com/aws/aws-sdk-go-v2/credentials"
2023-06-23 13:11:57 +00:00
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"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 . ` ,
2024-10-29 13:26:01 +00:00
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' ` ,
2023-06-23 13:11:57 +00:00
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"
2024-10-29 13:26:01 +00:00
headerFlag = "header"
2024-04-23 11:49:34 +00:00
sigV4AFlag = "sigv4a"
2023-06-23 13:11:57 +00:00
)
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)" )
2024-10-29 13:26:01 +00:00
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)" )
2024-04-23 11:49:34 +00:00
generatePresignedURLCmd . Flags ( ) . Bool ( sigV4AFlag , false , "Use SigV4A for signing request" )
2023-06-23 13:11:57 +00:00
_ = generatePresignedURLCmd . MarkFlagRequired ( endpointFlag )
_ = generatePresignedURLCmd . MarkFlagRequired ( bucketFlag )
_ = generatePresignedURLCmd . MarkFlagRequired ( objectFlag )
}
func runGeneratePresignedURLCmd ( * cobra . Command , [ ] string ) error {
var cfg aws . Config
if region := viper . GetString ( regionFlag ) ; region != "" {
cfg . Region = & region
}
accessKeyID := viper . GetString ( awsAccessKeyIDFlag )
secretAccessKey := viper . GetString ( awsSecretAccessKeyFlag )
if accessKeyID != "" && secretAccessKey != "" {
cfg . Credentials = credentials . NewStaticCredentialsFromCreds ( credentials . Value {
AccessKeyID : accessKeyID ,
SecretAccessKey : secretAccessKey ,
} )
}
sess , err := session . NewSessionWithOptions ( session . Options {
Config : cfg ,
Profile : viper . GetString ( profileFlag ) ,
SharedConfigState : session . SharedConfigEnable ,
} )
if err != nil {
2023-09-04 18:01:56 +00:00
return wrapPreparationError ( fmt . Errorf ( "couldn't get aws credentials: %w" , err ) )
2023-06-23 13:11:57 +00:00
}
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 : * sess . Config . Region ,
Lifetime : viper . GetDuration ( lifetimeFlag ) ,
SignTime : time . Now ( ) . UTC ( ) ,
}
2024-10-29 13:26:01 +00:00
headers , err := parseHeaders ( )
if err != nil {
return wrapPreparationError ( fmt . Errorf ( "failed to parse headers: %s" , err ) )
}
presignData . Headers = headers
2024-04-23 11:49:34 +00:00
var req * http . Request
if viper . GetBool ( sigV4AFlag ) {
val , err := sess . Config . Credentials . Get ( )
if err != nil {
return wrapPreparationError ( err )
}
awsCreds := credentialsv2 . NewStaticCredentialsProvider ( val . AccessKeyID , val . SecretAccessKey , "" )
req , err = auth . PresignRequestV4a ( awsCreds , reqData , presignData )
} else {
req , err = auth . PresignRequest ( sess . Config . Credentials , reqData , presignData )
}
2023-06-23 13:11:57 +00:00
if err != nil {
2023-09-04 18:01:56 +00:00
return wrapBusinessLogicError ( err )
2023-06-23 13:11:57 +00:00
}
res := & struct { URL string } {
URL : req . URL . String ( ) ,
}
enc := json . NewEncoder ( os . Stdout )
enc . SetIndent ( "" , " " )
enc . SetEscapeHTML ( false )
2023-09-04 18:01:56 +00:00
err = enc . Encode ( res )
if err != nil {
return wrapBusinessLogicError ( err )
}
return nil
2023-06-23 13:11:57 +00:00
}
2024-10-29 13:26:01 +00:00
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
}