diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..03ffa5d --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,39 @@ +name: Build and Release + +on: + workflow_run: + workflows: + - Create Tag + types: + - completed + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Go + uses: actions/setup-go@v4 + with: + go-version-file: go.mod + - name: Version + run: echo "::set-output name=version::$(cat VERSION)" + id: version + - name: Build artifacts + run: make build + - name: Create release + uses: softprops/action-gh-release@v1 + if: ${{ github.event.workflow_run.conclusion == 'success' }} + with: + draft: false + files: | + ./dist/darwin_amd64/presign + ./dist/darwin_arm64/presign + ./dist/linux_amd64/presign + ./dist/windows_amd64/presign.exe + generate_release_notes: true + name: ${{ steps.version.outputs.version }} + prerelease: false + tag_name: v${{ steps.version.outputs.version }} diff --git a/.github/workflows/tag.yml b/.github/workflows/tag.yml new file mode 100644 index 0000000..2df1d45 --- /dev/null +++ b/.github/workflows/tag.yml @@ -0,0 +1,23 @@ +name: Create Tag + +on: + push: + branches: + - main + +jobs: + create_tag: + name: Create and Push Tag + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Version + run: echo "::set-output name=version::$(cat VERSION)" + id: version + - name: Tag + run: | + git config user.name github-actions + git config user.email github-actions@github.com + git tag -a v${{ steps.version.outputs.version }} -m "${{ steps.version.outputs.version }}" + git push origin v${{ steps.version.outputs.version }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..849ddff --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +dist/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..59a89df --- /dev/null +++ b/Makefile @@ -0,0 +1,16 @@ +build: pkg-linux pkg-macos-intel pkg-macos-arm pkg-windows + +pkg-linux: + GOOS=linux GOARCH=amd64 go build -o dist/linux_amd64/presign + +pkg-macos-intel: + GOOS=darwin GOARCH=amd64 go build -o dist/darwin_amd64/presign + +pkg-macos-arm: + GOOS=darwin GOARCH=arm64 go build -o dist/darwin_arm64/presign + +pkg-windows: + GOOS=windows GOARCH=amd64 go build -o dist/windows_amd64/presign.exe + +clean: + rm -rf dist diff --git a/README.md b/README.md new file mode 100644 index 0000000..5906b72 --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# aws-presigner + +A simple command-line tool to create AWS S3 presigned URLs. The main motivation to create this was +because creating PUT URLs is not possible via the console or AWS CLI tool. + +## Usage + +To use, download the executable file and run like this: + +```sh +./presign -b rusher-test -k some-file.txt -m put +``` + +If you get a permission error, you made need to update the file permissions + +```sh +chmod +x presign +``` + +## Switching environments + +This tool loads the default AWS config, so it relies on whichever AWS CLI profile is currently set in your enviroment. To use a different environment without globally changing your aws profile, you can prepend the command to set the `AWS_PROFILE` environment variable for the execution + +```sh +AWS_PROFILE=qa ./presign -b rusher-test -k some-file.txt -m put +``` diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..6e8bf73 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.1.0 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1b0948f --- /dev/null +++ b/go.mod @@ -0,0 +1,27 @@ +module github.com/stori-rusher/aws-presigner + +go 1.21.4 + +require ( + github.com/aws/aws-sdk-go-v2 v1.24.0 + github.com/aws/aws-sdk-go-v2/config v1.26.0 + github.com/aws/aws-sdk-go-v2/service/s3 v1.47.4 +) + +require ( + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.16.11 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.7.1 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.9 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.9 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.9 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.18.4 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.4 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.26.4 // indirect + github.com/aws/smithy-go v1.19.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..00a5c4a --- /dev/null +++ b/go.sum @@ -0,0 +1,38 @@ +github.com/aws/aws-sdk-go-v2 v1.24.0 h1:890+mqQ+hTpNuw0gGP6/4akolQkSToDJgHfQE7AwGuk= +github.com/aws/aws-sdk-go-v2 v1.24.0/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4 h1:OCs21ST2LrepDfD3lwlQiOqIGp6JiEUqG84GzTDoyJs= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4/go.mod h1:usURWEKSNNAcAZuzRn/9ZYPT8aZQkR7xcCtunK/LkJo= +github.com/aws/aws-sdk-go-v2/config v1.26.0 h1:uItWWbD/FmHPGSa6GJFyZJD/RPakVjS0fmoq1vccjNw= +github.com/aws/aws-sdk-go-v2/config v1.26.0/go.mod h1:8Rf77VTcX9MMkoMIsCnuwmef+Y1bs2Zhvw9IXHdD/Po= +github.com/aws/aws-sdk-go-v2/credentials v1.16.11 h1:Gcut3tJSU7F/C5W/NnFimqnJqljF58rmaw7QlbigN3U= +github.com/aws/aws-sdk-go-v2/credentials v1.16.11/go.mod h1:CysUbSCfqvEbEQTd9Ubg2RrJy2EFM+AUHJOqqj0guTo= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10 h1:w98BT5w+ao1/r5sUuiH6JkVzjowOKeOJRHERyy1vh58= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10/go.mod h1:K2WGI7vUvkIv1HoNbfBA1bvIZ+9kL3YVmWxeKuLQsiw= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9 h1:v+HbZaCGmOwnTTVS86Fleq0vPzOd7tnJGbFhP0stNLs= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9/go.mod h1:Xjqy+Nyj7VDLBtCMkQYOw1QYfAEZCVLrfI0ezve8wd4= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9 h1:N94sVhRACtXyVcjXxrwK1SKFIJrA9pOJ5yu2eSHnmls= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9/go.mod h1:hqamLz7g1/4EJP+GH5NBhcUMLjW+gKLQabgyz6/7WAU= +github.com/aws/aws-sdk-go-v2/internal/ini v1.7.1 h1:uR9lXYjdPX0xY+NhvaJ4dD8rpSRz5VY81ccIIoNG+lw= +github.com/aws/aws-sdk-go-v2/internal/ini v1.7.1/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.9 h1:ugD6qzjYtB7zM5PN/ZIeaAIyefPaD82G8+SJopgvUpw= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.9/go.mod h1:YD0aYBWCrPENpHolhKw2XDlTIWae2GKXT1T4o6N6hiM= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 h1:/b31bi3YVNlkzkBrm9LfpaKoaYZUxIAj4sHfOTmLfqw= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4/go.mod h1:2aGXHFmbInwgP9ZfpmdIfOELL79zhdNYNmReK8qDfdQ= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.9 h1:/90OR2XbSYfXucBMJ4U14wrjlfleq/0SB6dZDPncgmo= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.9/go.mod h1:dN/Of9/fNZet7UrQQ6kTDo/VSwKPIq94vjlU16bRARc= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9 h1:Nf2sHxjMJR8CSImIVCONRi4g0Su3J+TSTbS7G0pUeMU= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9/go.mod h1:idky4TER38YIjr2cADF1/ugFMKvZV7p//pVeV5LZbF0= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.9 h1:iEAeF6YC3l4FzlJPP9H3Ko1TXpdjdqWffxXjp8SY6uk= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.9/go.mod h1:kjsXoK23q9Z/tLBrckZLLyvjhZoS+AGrzqzUfEClvMM= +github.com/aws/aws-sdk-go-v2/service/s3 v1.47.4 h1:iEkLh6fe2ATtH5PGynlJ1SdnbZuZgoWLdvSedjwmqKk= +github.com/aws/aws-sdk-go-v2/service/s3 v1.47.4/go.mod h1:vADO6Jn+Rq4nDtfwNjhgR84qkZwiC6FqCaXdw/kYwjA= +github.com/aws/aws-sdk-go-v2/service/sso v1.18.4 h1:2UVO4N/polvKeP+yCA8TLEmidEKxmNTeVpsZnj/bbgA= +github.com/aws/aws-sdk-go-v2/service/sso v1.18.4/go.mod h1:CaFfXLYL376jgbP7VKC96uFcU8Rlavak0UlAwk1Dlhc= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.4 h1:3JXkQ1F5n73qTpSPas6AQ8/6HFksgnB24JlNPLt3SlM= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.4/go.mod h1:W+nd4wWDVkSUIox9bacmkBP5NMFQeTJ/xqNabpzSR38= +github.com/aws/aws-sdk-go-v2/service/sts v1.26.4 h1:gaRFldXhoT36jVMfQ+AjAYwSfjO5LMgy1u0ObcKFhhc= +github.com/aws/aws-sdk-go-v2/service/sts v1.26.4/go.mod h1:XX5gh4CB7wAs4KhcF46G6C8a2i7eupU19dcAAE+EydU= +github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM= +github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= diff --git a/main.go b/main.go new file mode 100644 index 0000000..655ea23 --- /dev/null +++ b/main.go @@ -0,0 +1,82 @@ +package main + +import ( + "context" + _ "embed" + "flag" + "fmt" + "log" + "os" + "strings" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/s3" +) + +//go:embed VERSION +var version string + +func main() { + ctx := context.Background() + + var ( + b, k, m string + d int64 + v bool + ) + flag.StringVar(&b, "b", "", "bucket") + flag.Int64Var(&d, "d", 0, "duration in seconds (max 604,800)") + flag.StringVar(&k, "k", "", "key") + flag.StringVar(&m, "m", "", "method (get|put)") + flag.BoolVar(&v, "v", false, "version") + + flag.Usage = func() { + fmt.Fprintf(flag.CommandLine.Output(), "Usage of %s:\n", os.Args[0]) + flag.PrintDefaults() + fmt.Printf("\nVersion:\n %s\n", version) + } + + flag.Parse() + + if v { + fmt.Printf("v%s\n", version) + os.Exit(0) + } + + awsCFG, err := config.LoadDefaultConfig(ctx) + if err != nil { + log.Fatalf("unable to load S3 config, %v", err) + } + + s3Client := s3.NewFromConfig(awsCFG) + + loc, err := s3Client.GetBucketLocation(ctx, &s3.GetBucketLocationInput{Bucket: aws.String(b)}) + if err != nil { + log.Fatalf("unable to get bucket location: %v\n", err) + } + + p := NewS3Presigner(s3.NewPresignClient(s3Client), string(loc.LocationConstraint)) + + if d < 1 || d > 604_800 { + log.Fatalf("duration must be between 1 and 604,800 seconds (7 days)") + } + + var obj *v4.PresignedHTTPRequest + switch strings.ToLower(m) { + case "get": + obj, err = p.GetObject(ctx, b, k, time.Duration(d)*time.Second) + case "put": + obj, err = p.PutObject(ctx, b, k, time.Duration(d)*time.Second) + default: + log.Fatalf("invalid method: %v", m) + } + + if err != nil { + log.Fatalf("unable to presign request: %v\n", err) + } + + fmt.Println(obj.URL) +} diff --git a/presigner.go b/presigner.go new file mode 100644 index 0000000..82e982c --- /dev/null +++ b/presigner.go @@ -0,0 +1,53 @@ +package main + +import ( + "context" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4" + "github.com/aws/aws-sdk-go-v2/service/s3" +) + +type Presigner interface { + PresignGetObject(context.Context, *s3.GetObjectInput, ...func(*s3.PresignOptions)) (*v4.PresignedHTTPRequest, error) + PresignPutObject(context.Context, *s3.PutObjectInput, ...func(*s3.PresignOptions)) (*v4.PresignedHTTPRequest, error) +} + +// S3Presigner encapsulates the Amazon Simple Storage Service (Amazon S3) presign actions +// used in the examples. +// It contains PresignClient, a client that is used to presign requests to Amazon S3. +// Presigned requests contain temporary credentials and can be made from any HTTP client. +type S3Presigner struct { + client *s3.PresignClient + region string +} + +func NewS3Presigner(client *s3.PresignClient, region string) S3Presigner { + return S3Presigner{client, region} +} + +// GetObject makes a presigned request that can be used to get an object from a bucket. +// The presigned request is valid for the specified number of seconds. +func (p S3Presigner) GetObject(ctx context.Context, bucket, key string, exp time.Duration) (*v4.PresignedHTTPRequest, error) { + input := &s3.GetObjectInput{Bucket: aws.String(bucket), Key: aws.String(key)} + + return p.client.PresignGetObject(ctx, input, s3.WithPresignExpires(exp), withRegion(p.region)) +} + +// PutObject makes a presigned request that can be used to put an object in a bucket. +// The presigned request is valid for the specified number of seconds. +func (p S3Presigner) PutObject(ctx context.Context, bucket, key string, exp time.Duration) (*v4.PresignedHTTPRequest, error) { + input := &s3.PutObjectInput{Bucket: aws.String(bucket), Key: aws.String(key)} + + return p.client.PresignPutObject(ctx, input, s3.WithPresignExpires(exp), withRegion(p.region)) +} + +// withRegion is a helper function that adds the region to the PresignOptions. +func withRegion(region string) func(*s3.PresignOptions) { + return func(o *s3.PresignOptions) { + o.ClientOptions = append(o.ClientOptions, + func(o *s3.Options) { o.Region = region }, + ) + } +}