mirror of
https://github.com/versity/versitygw.git
synced 2026-01-27 13:32:02 +00:00
Compare commits
39 Commits
v0.3
...
audit-logg
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82cc95e9b9 | ||
|
|
bdf6e93510 | ||
|
|
8f81f4aa3d | ||
|
|
18899f8029 | ||
|
|
ca28792458 | ||
|
|
8c469cbd69 | ||
|
|
ff4bf23b6b | ||
|
|
38ddbc4712 | ||
|
|
cb193c42b4 | ||
|
|
fbafc6b34c | ||
|
|
d26b8856c1 | ||
|
|
23f738f37f | ||
|
|
a10729b3ff | ||
|
|
0330685c5c | ||
|
|
47dea2db7c | ||
|
|
db484eb900 | ||
|
|
140d41de40 | ||
|
|
39803cb158 | ||
|
|
9c858b0396 | ||
|
|
f63545c9b7 | ||
|
|
2894d4d5f3 | ||
|
|
46097fbf70 | ||
|
|
9db01362a0 | ||
|
|
fbd7bce530 | ||
|
|
7e34078d6a | ||
|
|
3c69c6922a | ||
|
|
08db927634 | ||
|
|
6d99c69953 | ||
|
|
4bfb3d84d3 | ||
|
|
30dbd02a83 | ||
|
|
f8afeec0a0 | ||
|
|
45e3c0922d | ||
|
|
a3f95520a8 | ||
|
|
c45280b7db | ||
|
|
77b0759f86 | ||
|
|
1da0c1ceba | ||
|
|
1d476c6d4d | ||
|
|
c4f5f958eb | ||
|
|
f84cfe58e7 |
27
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
27
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior.
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Server Version**
|
||||
output of
|
||||
```
|
||||
./versitygw -version
|
||||
uname -a
|
||||
```
|
||||
|
||||
**Additional context**
|
||||
Describe s3 client and version if applicable.
|
||||
14
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
14
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
30
.github/workflows/functional.yml
vendored
Normal file
30
.github/workflows/functional.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
name: functional tests
|
||||
on: pull_request
|
||||
jobs:
|
||||
|
||||
build:
|
||||
name: RunTests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 'stable'
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Get Dependencies
|
||||
run: |
|
||||
go get -v -t -d ./...
|
||||
|
||||
- name: Build and Run
|
||||
run: |
|
||||
make testbin
|
||||
./runtests.sh
|
||||
|
||||
- name: Coverage Report
|
||||
run: |
|
||||
go tool covdata percent -i=/tmp/covdata
|
||||
4
.github/workflows/go.yml
vendored
4
.github/workflows/go.yml
vendored
@@ -24,10 +24,10 @@ jobs:
|
||||
go get -v -t -d ./...
|
||||
|
||||
- name: Build
|
||||
run: go build -o versitygw cmd/versitygw/*.go
|
||||
run: make
|
||||
|
||||
- name: Test
|
||||
run: go test -v -timeout 30s -tags=github ./...
|
||||
run: go test -coverprofile profile.txt -race -v -timeout 30s -tags=github ./...
|
||||
|
||||
- name: Install govulncheck
|
||||
run: go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -32,3 +32,5 @@ VERSION
|
||||
/versitygw.spec
|
||||
*.tar
|
||||
*.tar.gz
|
||||
**/rand.data
|
||||
/profile.txt
|
||||
|
||||
3
Makefile
3
Makefile
@@ -34,6 +34,9 @@ build: $(BIN)
|
||||
$(BIN):
|
||||
$(GOBUILD) $(LDFLAGS) -o $(BIN) cmd/$(BIN)/*.go
|
||||
|
||||
testbin:
|
||||
$(GOBUILD) $(LDFLAGS) -o $(BIN) -cover -race cmd/$(BIN)/*.go
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
$(GOTEST) ./...
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# The Versity Gateway: A High-Performance Open Source S3 to File Translation Tool
|
||||
# The Versity Gateway:<br/>A High-Performance S3 to Storage System Translation Service
|
||||
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/versity/versitygw/blob/assets/assets/logo-white.svg">
|
||||
@@ -8,13 +8,11 @@
|
||||
|
||||
[](https://github.com/versity/versitygw/blob/main/LICENSE)
|
||||
|
||||
The Versity Gateway: A High-Performance Open Source S3 to File Translation Tool
|
||||
|
||||
Current status: Alpha, in development not yet suited for production use
|
||||
**Current status:** Alpha, in development not yet suited for production use
|
||||
|
||||
See project [documentation](https://github.com/versity/versitygw/wiki) on the wiki.
|
||||
|
||||
Versity Gateway, a simple to use tool for seamless inline translation between AWS S3 object commands and file-based storage systems. The Versity Gateway bridges the gap between S3-reliant applications and file storage systems, enabling enhanced compatibility and integration with file based systems while offering exceptional scalability.
|
||||
Versity Gateway, a simple to use tool for seamless inline translation between AWS S3 object commands and storage systems. The Versity Gateway bridges the gap between S3-reliant applications and other storage systems, enabling enhanced compatibility and integration while offering exceptional scalability.
|
||||
|
||||
The server translates incoming S3 API requests and transforms them into equivalent operations to the backend service. By leveraging this gateway server, applications can interact with the S3-compatible API on top of already existing storage systems. This project enables leveraging existing infrastructure investments while seamlessly integrating with S3-compatible systems, offering increased flexibility and compatibility in managing data storage.
|
||||
|
||||
|
||||
129
auth/acl.go
129
auth/acl.go
@@ -42,7 +42,11 @@ type GetBucketAclOutput struct {
|
||||
}
|
||||
|
||||
type AccessControlList struct {
|
||||
Grants []types.Grant
|
||||
Grants []types.Grant `xml:"Grant"`
|
||||
}
|
||||
type AccessControlPolicy struct {
|
||||
AccessControlList AccessControlList `xml:"AccessControlList"`
|
||||
Owner types.Owner
|
||||
}
|
||||
|
||||
func ParseACL(data []byte) (ACL, error) {
|
||||
@@ -80,69 +84,88 @@ func ParseACLOutput(data []byte) (GetBucketAclOutput, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func UpdateACL(input *s3.PutBucketAclInput, acl ACL, iam IAMService) error {
|
||||
func UpdateACL(input *s3.PutBucketAclInput, acl ACL, iam IAMService) ([]byte, error) {
|
||||
if input == nil {
|
||||
return nil, s3err.GetAPIError(s3err.ErrInvalidRequest)
|
||||
}
|
||||
if acl.Owner != *input.AccessControlPolicy.Owner.ID {
|
||||
return s3err.GetAPIError(s3err.ErrAccessDenied)
|
||||
return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
|
||||
}
|
||||
|
||||
// if the ACL is specified, set the ACL, else replace the grantees
|
||||
if input.ACL != "" {
|
||||
acl.ACL = input.ACL
|
||||
acl.Grantees = []Grantee{}
|
||||
return nil
|
||||
} else {
|
||||
grantees := []Grantee{}
|
||||
accs := []string{}
|
||||
|
||||
if input.GrantRead != nil {
|
||||
fullControlList, readList, readACPList, writeList, writeACPList := []string{}, []string{}, []string{}, []string{}, []string{}
|
||||
|
||||
if *input.GrantFullControl != "" {
|
||||
fullControlList = splitUnique(*input.GrantFullControl, ",")
|
||||
fmt.Println(fullControlList)
|
||||
for _, str := range fullControlList {
|
||||
grantees = append(grantees, Grantee{Access: str, Permission: "FULL_CONTROL"})
|
||||
}
|
||||
}
|
||||
if *input.GrantRead != "" {
|
||||
readList = splitUnique(*input.GrantRead, ",")
|
||||
for _, str := range readList {
|
||||
grantees = append(grantees, Grantee{Access: str, Permission: "READ"})
|
||||
}
|
||||
}
|
||||
if *input.GrantReadACP != "" {
|
||||
readACPList = splitUnique(*input.GrantReadACP, ",")
|
||||
for _, str := range readACPList {
|
||||
grantees = append(grantees, Grantee{Access: str, Permission: "READ_ACP"})
|
||||
}
|
||||
}
|
||||
if *input.GrantWrite != "" {
|
||||
writeList = splitUnique(*input.GrantWrite, ",")
|
||||
for _, str := range writeList {
|
||||
grantees = append(grantees, Grantee{Access: str, Permission: "WRITE"})
|
||||
}
|
||||
}
|
||||
if *input.GrantWriteACP != "" {
|
||||
writeACPList = splitUnique(*input.GrantWriteACP, ",")
|
||||
for _, str := range writeACPList {
|
||||
grantees = append(grantees, Grantee{Access: str, Permission: "WRITE_ACP"})
|
||||
}
|
||||
}
|
||||
|
||||
accs = append(append(append(append(fullControlList, readList...), writeACPList...), readACPList...), writeList...)
|
||||
} else {
|
||||
cache := make(map[string]bool)
|
||||
for _, grt := range input.AccessControlPolicy.Grants {
|
||||
grantees = append(grantees, Grantee{Access: *grt.Grantee.ID, Permission: grt.Permission})
|
||||
if _, ok := cache[*grt.Grantee.ID]; !ok {
|
||||
cache[*grt.Grantee.ID] = true
|
||||
accs = append(accs, *grt.Grantee.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the specified accounts exist
|
||||
accList, err := checkIfAccountsExist(accs, iam)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(accList) > 0 {
|
||||
return nil, fmt.Errorf("accounts does not exist: %s", strings.Join(accList, ", "))
|
||||
}
|
||||
|
||||
acl.Grantees = grantees
|
||||
acl.ACL = ""
|
||||
}
|
||||
|
||||
grantees := []Grantee{}
|
||||
|
||||
fullControlList, readList, readACPList, writeList, writeACPList := []string{}, []string{}, []string{}, []string{}, []string{}
|
||||
|
||||
if *input.GrantFullControl != "" {
|
||||
fullControlList = splitUnique(*input.GrantFullControl, ",")
|
||||
fmt.Println(fullControlList)
|
||||
for _, str := range fullControlList {
|
||||
grantees = append(grantees, Grantee{Access: str, Permission: "FULL_CONTROL"})
|
||||
}
|
||||
}
|
||||
if *input.GrantRead != "" {
|
||||
readList = splitUnique(*input.GrantRead, ",")
|
||||
for _, str := range readList {
|
||||
grantees = append(grantees, Grantee{Access: str, Permission: "READ"})
|
||||
}
|
||||
}
|
||||
if *input.GrantReadACP != "" {
|
||||
readACPList = splitUnique(*input.GrantReadACP, ",")
|
||||
for _, str := range readACPList {
|
||||
grantees = append(grantees, Grantee{Access: str, Permission: "READ_ACP"})
|
||||
}
|
||||
}
|
||||
if *input.GrantWrite != "" {
|
||||
writeList = splitUnique(*input.GrantWrite, ",")
|
||||
for _, str := range writeList {
|
||||
grantees = append(grantees, Grantee{Access: str, Permission: "WRITE"})
|
||||
}
|
||||
}
|
||||
if *input.GrantWriteACP != "" {
|
||||
writeACPList = splitUnique(*input.GrantWriteACP, ",")
|
||||
for _, str := range writeACPList {
|
||||
grantees = append(grantees, Grantee{Access: str, Permission: "WRITE_ACP"})
|
||||
}
|
||||
}
|
||||
|
||||
accs := append(append(append(append(fullControlList, readList...), writeACPList...), readACPList...), writeList...)
|
||||
|
||||
// Check if the specified accounts exist
|
||||
accList, err := checkIfAccountsExist(accs, iam)
|
||||
result, err := json.Marshal(acl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(accList) > 0 {
|
||||
return fmt.Errorf("accounts does not exist: %s", strings.Join(accList, ", "))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
acl.Grantees = grantees
|
||||
acl.ACL = ""
|
||||
|
||||
return nil
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func checkIfAccountsExist(accs []string, iam IAMService) ([]string, error) {
|
||||
@@ -153,7 +176,7 @@ func checkIfAccountsExist(accs []string, iam IAMService) ([]string, error) {
|
||||
if err != nil && err != ErrNoSuchUser {
|
||||
return nil, fmt.Errorf("check user account: %w", err)
|
||||
}
|
||||
if err == nil {
|
||||
if err == ErrNoSuchUser {
|
||||
result = append(result, acc)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,8 @@ type Account struct {
|
||||
}
|
||||
|
||||
// IAMService is the interface for all IAM service implementations
|
||||
//
|
||||
//go:generate moq -out ../s3api/controllers/iam_moq_test.go -pkg controllers . IAMService
|
||||
type IAMService interface {
|
||||
CreateAccount(access string, account Account) error
|
||||
GetUserAccount(access string) (Account, error)
|
||||
|
||||
@@ -76,7 +76,7 @@ func (s *IAMServiceInternal) CreateAccount(access string, account Account) error
|
||||
return nil, fmt.Errorf("failed to parse iam: %w", err)
|
||||
}
|
||||
} else {
|
||||
conf.AccessAccounts = make(map[string]Account)
|
||||
conf = IAMConfig{AccessAccounts: map[string]Account{}}
|
||||
}
|
||||
|
||||
_, ok := conf.AccessAccounts[access]
|
||||
@@ -85,10 +85,11 @@ func (s *IAMServiceInternal) CreateAccount(access string, account Account) error
|
||||
}
|
||||
conf.AccessAccounts[access] = account
|
||||
|
||||
b, err := json.Marshal(s.accts)
|
||||
b, err := json.Marshal(conf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to serialize iam: %w", err)
|
||||
}
|
||||
s.accts = conf
|
||||
|
||||
return b, nil
|
||||
})
|
||||
@@ -168,11 +169,13 @@ func (s *IAMServiceInternal) DeleteUserAccount(access string) error {
|
||||
|
||||
delete(conf.AccessAccounts, access)
|
||||
|
||||
b, err := json.Marshal(s.accts)
|
||||
b, err := json.Marshal(conf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to serialize iam: %w", err)
|
||||
}
|
||||
|
||||
s.accts = conf
|
||||
|
||||
return b, nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ import (
|
||||
"github.com/versity/versitygw/s3response"
|
||||
)
|
||||
|
||||
//go:generate moq -out backend_moq_test.go . Backend
|
||||
//go:generate moq -out ../s3api/controllers/backend_moq_test.go -pkg controllers . Backend
|
||||
type Backend interface {
|
||||
fmt.Stringer
|
||||
@@ -40,18 +39,17 @@ type Backend interface {
|
||||
CreateMultipartUpload(*s3.CreateMultipartUploadInput) (*s3.CreateMultipartUploadOutput, error)
|
||||
CompleteMultipartUpload(bucket, object, uploadID string, parts []types.Part) (*s3.CompleteMultipartUploadOutput, error)
|
||||
AbortMultipartUpload(*s3.AbortMultipartUploadInput) error
|
||||
ListMultipartUploads(output *s3.ListMultipartUploadsInput) (s3response.ListMultipartUploadsResponse, error)
|
||||
ListMultipartUploads(*s3.ListMultipartUploadsInput) (s3response.ListMultipartUploadsResponse, error)
|
||||
ListObjectParts(bucket, object, uploadID string, partNumberMarker int, maxParts int) (s3response.ListPartsResponse, error)
|
||||
CopyPart(srcBucket, srcObject, DstBucket, uploadID, rangeHeader string, part int) (*types.CopyPartResult, error)
|
||||
PutObjectPart(bucket, object, uploadID string, part int, length int64, r io.Reader) (etag string, err error)
|
||||
UploadPartCopy(*s3.UploadPartCopyInput) (*s3.UploadPartCopyOutput, error)
|
||||
UploadPartCopy(*s3.UploadPartCopyInput) (s3response.CopyObjectResult, error)
|
||||
|
||||
PutObject(*s3.PutObjectInput) (string, error)
|
||||
HeadObject(bucket, object string) (*s3.HeadObjectOutput, error)
|
||||
GetObject(bucket, object, acceptRange string, writer io.Writer) (*s3.GetObjectOutput, error)
|
||||
GetObjectAcl(bucket, object string) (*s3.GetObjectAclOutput, error)
|
||||
GetObjectAttributes(bucket, object string, attributes []string) (*s3.GetObjectAttributesOutput, error)
|
||||
CopyObject(srcBucket, srcObject, DstBucket, dstObject string) (*s3.CopyObjectOutput, error)
|
||||
CopyObject(srcBucket, srcObject, dstBucket, dstObject string) (*s3.CopyObjectOutput, error)
|
||||
ListObjects(bucket, prefix, marker, delim string, maxkeys int) (*s3.ListObjectsOutput, error)
|
||||
ListObjectsV2(bucket, prefix, marker, delim string, maxkeys int) (*s3.ListObjectsV2Output, error)
|
||||
DeleteObject(bucket, object string) error
|
||||
@@ -87,8 +85,8 @@ func (BackendUnsupported) PutObjectAcl(*s3.PutObjectAclInput) error {
|
||||
func (BackendUnsupported) RestoreObject(bucket, object string, restoreRequest *s3.RestoreObjectInput) error {
|
||||
return s3err.GetAPIError(s3err.ErrNotImplemented)
|
||||
}
|
||||
func (BackendUnsupported) UploadPartCopy(*s3.UploadPartCopyInput) (*s3.UploadPartCopyOutput, error) {
|
||||
return nil, s3err.GetAPIError(s3err.ErrNotImplemented)
|
||||
func (BackendUnsupported) UploadPartCopy(*s3.UploadPartCopyInput) (s3response.CopyObjectResult, error) {
|
||||
return s3response.CopyObjectResult{}, s3err.GetAPIError(s3err.ErrNotImplemented)
|
||||
}
|
||||
func (BackendUnsupported) GetBucketAcl(bucket string) ([]byte, error) {
|
||||
return nil, s3err.GetAPIError(s3err.ErrNotImplemented)
|
||||
@@ -118,9 +116,6 @@ func (BackendUnsupported) ListMultipartUploads(output *s3.ListMultipartUploadsIn
|
||||
func (BackendUnsupported) ListObjectParts(bucket, object, uploadID string, partNumberMarker int, maxParts int) (s3response.ListPartsResponse, error) {
|
||||
return s3response.ListPartsResponse{}, s3err.GetAPIError(s3err.ErrNotImplemented)
|
||||
}
|
||||
func (BackendUnsupported) CopyPart(srcBucket, srcObject, DstBucket, uploadID, rangeHeader string, part int) (*types.CopyPartResult, error) {
|
||||
return nil, s3err.GetAPIError(s3err.ErrNotImplemented)
|
||||
}
|
||||
func (BackendUnsupported) PutObjectPart(bucket, object, uploadID string, part int, length int64, r io.Reader) (etag string, err error) {
|
||||
return "", s3err.GetAPIError(s3err.ErrNotImplemented)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,222 +0,0 @@
|
||||
// Copyright 2023 Versity Software
|
||||
// This file is licensed under the Apache License, Version 2.0
|
||||
// (the "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package backend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
"github.com/versity/versitygw/s3err"
|
||||
"github.com/versity/versitygw/s3response"
|
||||
)
|
||||
|
||||
func TestBackend_ListBuckets(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
}
|
||||
type test struct {
|
||||
name string
|
||||
c Backend
|
||||
args args
|
||||
wantErr bool
|
||||
}
|
||||
var tests []test
|
||||
tests = append(tests, test{
|
||||
name: "list-Bucket",
|
||||
c: &BackendMock{
|
||||
ListBucketsFunc: func() (s3response.ListAllMyBucketsResult, error) {
|
||||
return s3response.ListAllMyBucketsResult{
|
||||
Buckets: s3response.ListAllMyBucketsList{
|
||||
Bucket: []s3response.ListAllMyBucketsEntry{
|
||||
{
|
||||
Name: "t1",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, s3err.GetAPIError(0)
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
},
|
||||
wantErr: false,
|
||||
}, test{
|
||||
name: "list-Bucket-error",
|
||||
c: &BackendMock{
|
||||
ListBucketsFunc: func() (s3response.ListAllMyBucketsResult, error) {
|
||||
return s3response.ListAllMyBucketsResult{}, s3err.GetAPIError(s3err.ErrNotImplemented)
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
},
|
||||
wantErr: true,
|
||||
})
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if _, err := tt.c.ListBuckets(); (err.(s3err.APIError).Code != "") != tt.wantErr {
|
||||
t.Errorf("Backend.ListBuckets() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
func TestBackend_HeadBucket(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
BucketName string
|
||||
}
|
||||
type test struct {
|
||||
name string
|
||||
c Backend
|
||||
args args
|
||||
wantErr bool
|
||||
}
|
||||
var tests []test
|
||||
tests = append(tests, test{
|
||||
name: "head-buckets-error",
|
||||
c: &BackendMock{
|
||||
HeadBucketFunc: func(bucket string) (*s3.HeadBucketOutput, error) {
|
||||
return nil, s3err.GetAPIError(s3err.ErrNotImplemented)
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
BucketName: "b1",
|
||||
},
|
||||
wantErr: true,
|
||||
})
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if _, err := tt.c.HeadBucket(tt.args.BucketName); (err.(s3err.APIError).Code != "") != tt.wantErr {
|
||||
t.Errorf("Backend.HeadBucket() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
func TestBackend_GetBucketAcl(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
bucketName string
|
||||
}
|
||||
type test struct {
|
||||
name string
|
||||
c Backend
|
||||
args args
|
||||
wantErr bool
|
||||
}
|
||||
var tests []test
|
||||
tests = append(tests, test{
|
||||
name: "get bucket acl error",
|
||||
c: &BackendMock{
|
||||
GetBucketAclFunc: func(bucket string) ([]byte, error) {
|
||||
return nil, s3err.GetAPIError(s3err.ErrNotImplemented)
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
bucketName: "b1",
|
||||
},
|
||||
wantErr: true,
|
||||
})
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if _, err := tt.c.GetBucketAcl(tt.args.bucketName); (err.(s3err.APIError).Code != "") != tt.wantErr {
|
||||
t.Errorf("Backend.GetBucketAcl() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
func TestBackend_PutBucket(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
bucketName string
|
||||
bucketOwner string
|
||||
}
|
||||
type test struct {
|
||||
name string
|
||||
c Backend
|
||||
args args
|
||||
wantErr bool
|
||||
}
|
||||
var tests []test
|
||||
tests = append(tests, test{
|
||||
name: "put bucket ",
|
||||
c: &BackendMock{
|
||||
PutBucketFunc: func(bucket, owner string) error {
|
||||
return s3err.GetAPIError(0)
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
bucketName: "b1",
|
||||
bucketOwner: "owner",
|
||||
},
|
||||
wantErr: false,
|
||||
}, test{
|
||||
name: "put bucket error",
|
||||
c: &BackendMock{
|
||||
PutBucketFunc: func(bucket, owner string) error {
|
||||
return s3err.GetAPIError(s3err.ErrNotImplemented)
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
bucketName: "b2",
|
||||
bucketOwner: "owner",
|
||||
},
|
||||
wantErr: true,
|
||||
})
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := tt.c.PutBucket(tt.args.bucketName, tt.args.bucketOwner); (err.(s3err.APIError).Code != "") != tt.wantErr {
|
||||
t.Errorf("Backend.PutBucket() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
func TestBackend_DeleteBucket(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
bucketName string
|
||||
}
|
||||
type test struct {
|
||||
name string
|
||||
c Backend
|
||||
args args
|
||||
wantErr bool
|
||||
}
|
||||
var tests []test
|
||||
tests = append(tests, test{
|
||||
name: "Delete Bucket Error",
|
||||
c: &BackendMock{
|
||||
DeleteBucketFunc: func(bucket string) error {
|
||||
return s3err.GetAPIError(s3err.ErrNotImplemented)
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
bucketName: "b1",
|
||||
},
|
||||
wantErr: true,
|
||||
})
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := tt.c.DeleteBucket(tt.args.bucketName); (err.(s3err.APIError).Code != "") != tt.wantErr {
|
||||
t.Errorf("Backend.DeleteBucket() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,6 @@ package backend
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"strconv"
|
||||
@@ -25,6 +24,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
||||
"github.com/versity/versitygw/s3err"
|
||||
"github.com/versity/versitygw/s3response"
|
||||
)
|
||||
|
||||
@@ -55,6 +55,12 @@ func GetTimePtr(t time.Time) *time.Time {
|
||||
return &t
|
||||
}
|
||||
|
||||
var (
|
||||
errInvalidRange = s3err.GetAPIError(s3err.ErrInvalidRequest)
|
||||
)
|
||||
|
||||
// ParseRange parses input range header and returns startoffset, length, and
|
||||
// error. If no endoffset specified, then length is set to -1.
|
||||
func ParseRange(file fs.FileInfo, acceptRange string) (int64, int64, error) {
|
||||
if acceptRange == "" {
|
||||
return 0, file.Size(), nil
|
||||
@@ -63,29 +69,34 @@ func ParseRange(file fs.FileInfo, acceptRange string) (int64, int64, error) {
|
||||
rangeKv := strings.Split(acceptRange, "=")
|
||||
|
||||
if len(rangeKv) < 2 {
|
||||
return 0, 0, errors.New("invalid range parameter")
|
||||
return 0, 0, errInvalidRange
|
||||
}
|
||||
|
||||
bRange := strings.Split(rangeKv[1], "-")
|
||||
if len(bRange) < 2 {
|
||||
return 0, 0, errors.New("invalid range parameter")
|
||||
if len(bRange) < 1 || len(bRange) > 2 {
|
||||
return 0, 0, errInvalidRange
|
||||
}
|
||||
|
||||
startOffset, err := strconv.ParseInt(bRange[0], 10, 64)
|
||||
if err != nil {
|
||||
return 0, 0, errors.New("invalid range parameter")
|
||||
return 0, 0, errInvalidRange
|
||||
}
|
||||
|
||||
endOffset, err := strconv.ParseInt(bRange[1], 10, 64)
|
||||
endOffset := int64(-1)
|
||||
if len(bRange) == 1 || bRange[1] == "" {
|
||||
return startOffset, endOffset, nil
|
||||
}
|
||||
|
||||
endOffset, err = strconv.ParseInt(bRange[1], 10, 64)
|
||||
if err != nil {
|
||||
return 0, 0, errors.New("invalid range parameter")
|
||||
return 0, 0, errInvalidRange
|
||||
}
|
||||
|
||||
if endOffset < startOffset {
|
||||
return 0, 0, errors.New("invalid range parameter")
|
||||
return 0, 0, errInvalidRange
|
||||
}
|
||||
|
||||
return int64(startOffset), int64(endOffset - startOffset + 1), nil
|
||||
return startOffset, endOffset - startOffset + 1, nil
|
||||
}
|
||||
|
||||
func GetMultipartMD5(parts []types.Part) string {
|
||||
|
||||
@@ -693,10 +693,6 @@ func (p *Posix) ListObjectParts(bucket, object, uploadID string, partNumberMarke
|
||||
}, nil
|
||||
}
|
||||
|
||||
// TODO: copy part
|
||||
// func (p *Posix) CopyPart(srcBucket, srcObject, DstBucket, uploadID, rangeHeader string, part int) (*types.CopyPartResult, error) {
|
||||
// }
|
||||
|
||||
func (p *Posix) PutObjectPart(bucket, object, uploadID string, part int, length int64, r io.Reader) (string, error) {
|
||||
_, err := os.Stat(bucket)
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
@@ -708,6 +704,15 @@ func (p *Posix) PutObjectPart(bucket, object, uploadID string, part int, length
|
||||
|
||||
sum := sha256.Sum256([]byte(object))
|
||||
objdir := filepath.Join(metaTmpMultipartDir, fmt.Sprintf("%x", sum))
|
||||
|
||||
_, err = os.Stat(filepath.Join(bucket, objdir, uploadID))
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return "", s3err.GetAPIError(s3err.ErrNoSuchUpload)
|
||||
}
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("stat uploadid: %w", err)
|
||||
}
|
||||
|
||||
partPath := filepath.Join(objdir, uploadID, fmt.Sprintf("%v", part))
|
||||
|
||||
f, err := openTmpFile(filepath.Join(bucket, objdir),
|
||||
@@ -736,6 +741,111 @@ func (p *Posix) PutObjectPart(bucket, object, uploadID string, part int, length
|
||||
return etag, nil
|
||||
}
|
||||
|
||||
func (p *Posix) UploadPartCopy(upi *s3.UploadPartCopyInput) (s3response.CopyObjectResult, error) {
|
||||
_, err := os.Stat(*upi.Bucket)
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return s3response.CopyObjectResult{}, s3err.GetAPIError(s3err.ErrNoSuchBucket)
|
||||
}
|
||||
if err != nil {
|
||||
return s3response.CopyObjectResult{}, fmt.Errorf("stat bucket: %w", err)
|
||||
}
|
||||
|
||||
sum := sha256.Sum256([]byte(*upi.Key))
|
||||
objdir := filepath.Join(metaTmpMultipartDir, fmt.Sprintf("%x", sum))
|
||||
|
||||
_, err = os.Stat(filepath.Join(*upi.Bucket, objdir, *upi.UploadId))
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return s3response.CopyObjectResult{}, s3err.GetAPIError(s3err.ErrNoSuchUpload)
|
||||
}
|
||||
if err != nil {
|
||||
return s3response.CopyObjectResult{}, fmt.Errorf("stat uploadid: %w", err)
|
||||
}
|
||||
|
||||
partPath := filepath.Join(objdir, *upi.UploadId, fmt.Sprintf("%v", upi.PartNumber))
|
||||
|
||||
substrs := strings.SplitN(*upi.CopySource, "/", 2)
|
||||
if len(substrs) != 2 {
|
||||
return s3response.CopyObjectResult{}, s3err.GetAPIError(s3err.ErrInvalidCopySource)
|
||||
}
|
||||
|
||||
srcBucket := substrs[0]
|
||||
srcObject := substrs[1]
|
||||
|
||||
_, err = os.Stat(srcBucket)
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return s3response.CopyObjectResult{}, s3err.GetAPIError(s3err.ErrNoSuchBucket)
|
||||
}
|
||||
if err != nil {
|
||||
return s3response.CopyObjectResult{}, fmt.Errorf("stat bucket: %w", err)
|
||||
}
|
||||
|
||||
objPath := filepath.Join(srcBucket, srcObject)
|
||||
fi, err := os.Stat(objPath)
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return s3response.CopyObjectResult{}, s3err.GetAPIError(s3err.ErrNoSuchKey)
|
||||
}
|
||||
if err != nil {
|
||||
return s3response.CopyObjectResult{}, fmt.Errorf("stat object: %w", err)
|
||||
}
|
||||
|
||||
startOffset, length, err := backend.ParseRange(fi, *upi.CopySourceRange)
|
||||
if err != nil {
|
||||
return s3response.CopyObjectResult{}, err
|
||||
}
|
||||
|
||||
if length == -1 {
|
||||
length = fi.Size() - startOffset + 1
|
||||
}
|
||||
|
||||
if startOffset+length > fi.Size()+1 {
|
||||
return s3response.CopyObjectResult{}, s3err.GetAPIError(s3err.ErrInvalidRequest)
|
||||
}
|
||||
|
||||
f, err := openTmpFile(filepath.Join(*upi.Bucket, objdir),
|
||||
*upi.Bucket, partPath, length)
|
||||
if err != nil {
|
||||
return s3response.CopyObjectResult{}, fmt.Errorf("open temp file: %w", err)
|
||||
}
|
||||
defer f.cleanup()
|
||||
|
||||
srcf, err := os.Open(objPath)
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return s3response.CopyObjectResult{}, s3err.GetAPIError(s3err.ErrNoSuchKey)
|
||||
}
|
||||
if err != nil {
|
||||
return s3response.CopyObjectResult{}, fmt.Errorf("open object: %w", err)
|
||||
}
|
||||
defer srcf.Close()
|
||||
|
||||
rdr := io.NewSectionReader(srcf, startOffset, length)
|
||||
hash := md5.New()
|
||||
tr := io.TeeReader(rdr, hash)
|
||||
|
||||
_, err = io.Copy(f, tr)
|
||||
if err != nil {
|
||||
return s3response.CopyObjectResult{}, fmt.Errorf("copy part data: %w", err)
|
||||
}
|
||||
|
||||
err = f.link()
|
||||
if err != nil {
|
||||
return s3response.CopyObjectResult{}, fmt.Errorf("link object in namespace: %w", err)
|
||||
}
|
||||
|
||||
dataSum := hash.Sum(nil)
|
||||
etag := hex.EncodeToString(dataSum)
|
||||
xattr.Set(filepath.Join(*upi.Bucket, partPath), etagkey, []byte(etag))
|
||||
|
||||
fi, err = os.Stat(filepath.Join(*upi.Bucket, partPath))
|
||||
if err != nil {
|
||||
return s3response.CopyObjectResult{}, fmt.Errorf("stat part path: %w", err)
|
||||
}
|
||||
|
||||
return s3response.CopyObjectResult{
|
||||
ETag: etag,
|
||||
LastModified: fi.ModTime(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *Posix) PutObject(po *s3.PutObjectInput) (string, error) {
|
||||
_, err := os.Stat(*po.Bucket)
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
@@ -811,7 +921,7 @@ func (p *Posix) DeleteObject(bucket, object string) error {
|
||||
return fmt.Errorf("stat bucket: %w", err)
|
||||
}
|
||||
|
||||
os.Remove(filepath.Join(bucket, object))
|
||||
err = os.Remove(filepath.Join(bucket, object))
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return s3err.GetAPIError(s3err.ErrNoSuchKey)
|
||||
}
|
||||
@@ -891,8 +1001,11 @@ func (p *Posix) GetObject(bucket, object, acceptRange string, writer io.Writer)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if startOffset+length > fi.Size() {
|
||||
// TODO: is ErrInvalidRequest correct here?
|
||||
if length == -1 {
|
||||
length = fi.Size() - startOffset + 1
|
||||
}
|
||||
|
||||
if startOffset+length > fi.Size()+1 {
|
||||
return nil, s3err.GetAPIError(s3err.ErrInvalidRequest)
|
||||
}
|
||||
|
||||
@@ -975,7 +1088,7 @@ func (p *Posix) HeadObject(bucket, object string) (*s3.HeadObjectOutput, error)
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *Posix) CopyObject(srcBucket, srcObject, DstBucket, dstObject string) (*s3.CopyObjectOutput, error) {
|
||||
func (p *Posix) CopyObject(srcBucket, srcObject, dstBucket, dstObject string) (*s3.CopyObjectOutput, error) {
|
||||
_, err := os.Stat(srcBucket)
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return nil, s3err.GetAPIError(s3err.ErrNoSuchBucket)
|
||||
@@ -984,7 +1097,7 @@ func (p *Posix) CopyObject(srcBucket, srcObject, DstBucket, dstObject string) (*
|
||||
return nil, fmt.Errorf("stat bucket: %w", err)
|
||||
}
|
||||
|
||||
_, err = os.Stat(DstBucket)
|
||||
_, err = os.Stat(dstBucket)
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return nil, s3err.GetAPIError(s3err.ErrNoSuchBucket)
|
||||
}
|
||||
@@ -1002,12 +1115,17 @@ func (p *Posix) CopyObject(srcBucket, srcObject, DstBucket, dstObject string) (*
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
etag, err := p.PutObject(&s3.PutObjectInput{Bucket: &DstBucket, Key: &dstObject, Body: f})
|
||||
fInfo, err := f.Stat()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("stat object: %w", err)
|
||||
}
|
||||
|
||||
etag, err := p.PutObject(&s3.PutObjectInput{Bucket: &dstBucket, Key: &dstObject, Body: f, ContentLength: fInfo.Size()})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fi, err := os.Stat(filepath.Join(DstBucket, dstObject))
|
||||
fi, err := os.Stat(filepath.Join(dstBucket, dstObject))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("stat dst object: %w", err)
|
||||
}
|
||||
@@ -1257,7 +1375,7 @@ func (p *Posix) InitIAM() error {
|
||||
|
||||
_, err := os.ReadFile(iamFile)
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
b, err := json.Marshal(auth.IAMConfig{})
|
||||
b, err := json.Marshal(auth.IAMConfig{AccessAccounts: map[string]auth.Account{}})
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal default iam: %w", err)
|
||||
}
|
||||
|
||||
@@ -440,8 +440,11 @@ func (s *ScoutFS) GetObject(bucket, object, acceptRange string, writer io.Writer
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if length == -1 {
|
||||
length = fi.Size() - startOffset + 1
|
||||
}
|
||||
|
||||
if startOffset+length > fi.Size() {
|
||||
// TODO: is ErrInvalidRequest correct here?
|
||||
return nil, s3err.GetAPIError(s3err.ErrInvalidRequest)
|
||||
}
|
||||
|
||||
|
||||
@@ -65,12 +65,6 @@ func adminCommand() *cli.Command {
|
||||
Required: true,
|
||||
Aliases: []string{"r"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "region",
|
||||
Usage: "s3 region string for the user",
|
||||
Value: "us-east-1",
|
||||
Aliases: []string{"rg"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -90,40 +84,40 @@ func adminCommand() *cli.Command {
|
||||
Flags: []cli.Flag{
|
||||
// TODO: create a configuration file for this
|
||||
&cli.StringFlag{
|
||||
Name: "adminAccess",
|
||||
Name: "access",
|
||||
Usage: "admin access account",
|
||||
EnvVars: []string{"ADMIN_ACCESS_KEY_ID", "ADMIN_ACCESS_KEY"},
|
||||
Aliases: []string{"aa"},
|
||||
Aliases: []string{"a"},
|
||||
Destination: &adminAccess,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "adminSecret",
|
||||
Name: "secret",
|
||||
Usage: "admin secret access key",
|
||||
EnvVars: []string{"ADMIN_SECRET_ACCESS_KEY", "ADMIN_SECRET_KEY"},
|
||||
Aliases: []string{"as"},
|
||||
Aliases: []string{"s"},
|
||||
Destination: &adminSecret,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "adminRegion",
|
||||
Name: "region",
|
||||
Usage: "s3 region string",
|
||||
Value: "us-east-1",
|
||||
Destination: &adminRegion,
|
||||
Aliases: []string{"ar"},
|
||||
Aliases: []string{"r"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func createUser(ctx *cli.Context) error {
|
||||
access, secret, role, region := ctx.String("access"), ctx.String("secret"), ctx.String("role"), ctx.String("region")
|
||||
if access == "" || secret == "" || region == "" {
|
||||
access, secret, role := ctx.String("access"), ctx.String("secret"), ctx.String("role")
|
||||
if access == "" || secret == "" {
|
||||
return fmt.Errorf("invalid input parameters for the new user")
|
||||
}
|
||||
if role != "admin" && role != "user" {
|
||||
return fmt.Errorf("invalid input parameter for role")
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:7070/create-user?access=%v&secret=%v&role=%v®ion=%v", access, secret, role, region), nil)
|
||||
req, err := http.NewRequest(http.MethodPatch, fmt.Sprintf("http://localhost:7070/create-user?access=%v&secret=%v&role=%v", access, secret, role), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send the request: %w", err)
|
||||
}
|
||||
@@ -163,7 +157,7 @@ func deleteUser(ctx *cli.Context) error {
|
||||
return fmt.Errorf("invalid input parameter for the new user")
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("http://localhost:7070/delete-user?access=%v", access), nil)
|
||||
req, err := http.NewRequest(http.MethodPatch, fmt.Sprintf("http://localhost:7070/delete-user?access=%v", access), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send the request: %w", err)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"log"
|
||||
@@ -26,6 +27,7 @@ import (
|
||||
"github.com/versity/versitygw/backend"
|
||||
"github.com/versity/versitygw/s3api"
|
||||
"github.com/versity/versitygw/s3api/middlewares"
|
||||
"github.com/versity/versitygw/s3log"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -34,6 +36,10 @@ var (
|
||||
rootUserSecret string
|
||||
region string
|
||||
certFile, keyFile string
|
||||
webhookLoggerURL string
|
||||
kafkaLoggerURL string
|
||||
kafkaMessageTopic string
|
||||
kafkaMessageKey string
|
||||
debug bool
|
||||
)
|
||||
|
||||
@@ -47,6 +53,8 @@ var (
|
||||
)
|
||||
|
||||
func main() {
|
||||
setupSignalHandler()
|
||||
|
||||
app := initApp()
|
||||
|
||||
app.Commands = []*cli.Command{
|
||||
@@ -56,7 +64,14 @@ func main() {
|
||||
testCommand(),
|
||||
}
|
||||
|
||||
if err := app.Run(os.Args); err != nil {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go func() {
|
||||
<-sigDone
|
||||
fmt.Fprintf(os.Stderr, "terminating signal caught, shutting down\n")
|
||||
cancel()
|
||||
}()
|
||||
|
||||
if err := app.RunContext(ctx, os.Args); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -131,10 +146,34 @@ func initFlags() []cli.Flag {
|
||||
Usage: "enable debug output",
|
||||
Destination: &debug,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "webhook-logger-url",
|
||||
Usage: "Webhook logger url to send audit logs",
|
||||
Destination: &webhookLoggerURL,
|
||||
Aliases: []string{"wlu"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "kafka-logger-url",
|
||||
Usage: "Kafka server url to send audit logs",
|
||||
Destination: &kafkaLoggerURL,
|
||||
Aliases: []string{"klu"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "kafka-message-topic",
|
||||
Usage: "Kafka server message topic to send the logs",
|
||||
Destination: &kafkaMessageTopic,
|
||||
Aliases: []string{"kmt"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "kafka-message-key",
|
||||
Usage: "Kafka server message topic key for partitioning",
|
||||
Destination: &kafkaMessageKey,
|
||||
Aliases: []string{"kmk"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func runGateway(be backend.Backend, s auth.Storer) error {
|
||||
func runGateway(ctx *cli.Context, be backend.Backend, s auth.Storer, storageSystem string) error {
|
||||
app := fiber.New(fiber.Config{
|
||||
AppName: "versitygw",
|
||||
ServerHeader: "VERSITYGW",
|
||||
@@ -172,13 +211,34 @@ func runGateway(be backend.Backend, s auth.Storer) error {
|
||||
return fmt.Errorf("setup internal iam service: %w", err)
|
||||
}
|
||||
|
||||
logger, err := s3log.InitLogger(&s3log.LogConfig{
|
||||
StorageSystem: storageSystem,
|
||||
WebhookURL: webhookLoggerURL,
|
||||
KafkaURL: kafkaLoggerURL,
|
||||
KafkaTopic: kafkaMessageTopic,
|
||||
KafkaTopicKey: kafkaMessageKey,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("setup logger: %w", err)
|
||||
}
|
||||
|
||||
srv, err := s3api.New(app, be, middlewares.RootUserConfig{
|
||||
Access: rootUserAccess,
|
||||
Secret: rootUserSecret,
|
||||
}, port, region, iam, opts...)
|
||||
}, port, region, iam, logger, opts...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("init gateway: %v", err)
|
||||
}
|
||||
|
||||
return srv.Serve()
|
||||
c := make(chan error, 1)
|
||||
go func() { c <- srv.Serve() }()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
be.Shutdown()
|
||||
return ctx.Err()
|
||||
case err := <-c:
|
||||
be.Shutdown()
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,5 +49,5 @@ func runPosix(ctx *cli.Context) error {
|
||||
return fmt.Errorf("init posix: %v", err)
|
||||
}
|
||||
|
||||
return runGateway(be, be)
|
||||
return runGateway(ctx, be, be, "Posix")
|
||||
}
|
||||
|
||||
@@ -69,5 +69,5 @@ func runScoutfs(ctx *cli.Context) error {
|
||||
return fmt.Errorf("init scoutfs: %v", err)
|
||||
}
|
||||
|
||||
return runGateway(be, be)
|
||||
return runGateway(ctx, be, be, "ScoutFS")
|
||||
}
|
||||
|
||||
42
cmd/versitygw/singal.go
Normal file
42
cmd/versitygw/singal.go
Normal file
@@ -0,0 +1,42 @@
|
||||
// Copyright 2023 Versity Software
|
||||
// This file is licensed under the Apache License, Version 2.0
|
||||
// (the "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
var (
|
||||
sigDone = make(chan bool, 1)
|
||||
)
|
||||
|
||||
func setupSignalHandler() {
|
||||
sigs := make(chan os.Signal, 1)
|
||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
|
||||
|
||||
go func() {
|
||||
for sig := range sigs {
|
||||
fmt.Fprintf(os.Stderr, "caught signal %v\n", sig)
|
||||
switch sig {
|
||||
case syscall.SIGINT, syscall.SIGTERM:
|
||||
sigDone <- true
|
||||
case syscall.SIGHUP:
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -8,9 +8,19 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
awsID string
|
||||
awsSecret string
|
||||
endpoint string
|
||||
awsID string
|
||||
awsSecret string
|
||||
endpoint string
|
||||
prefix string
|
||||
dstBucket string
|
||||
partSize int64
|
||||
objSize int64
|
||||
concurrency int
|
||||
files int
|
||||
upload bool
|
||||
download bool
|
||||
pathStyle bool
|
||||
checksumDisable bool
|
||||
)
|
||||
|
||||
func testCommand() *cli.Command {
|
||||
@@ -58,17 +68,20 @@ func initTestFlags() []cli.Flag {
|
||||
func initTestCommands() []*cli.Command {
|
||||
return []*cli.Command{
|
||||
{
|
||||
Name: "make-bucket",
|
||||
Usage: "Test bucket creation.",
|
||||
Name: "bucket-actions",
|
||||
Usage: "Test bucket creation, checking the existence, deletes it.",
|
||||
Description: `Calls s3 gateway create-bucket action to create a new bucket,
|
||||
then calls delete-bucket action to delete the bucket.`,
|
||||
calls head-bucket action to check the existence, then calls delete-bucket action to delete the bucket.`,
|
||||
Action: getAction(integration.TestMakeBucket),
|
||||
},
|
||||
{
|
||||
Name: "put-get-object",
|
||||
Usage: "Test put & get object.",
|
||||
Name: "object-actions",
|
||||
Usage: "Test put/get/delete/copy objects.",
|
||||
Description: `Creates a bucket with s3 gateway action, puts an object in it,
|
||||
gets the object from the bucket, deletes both the object and bucket.`,
|
||||
tries to copy into another bucket, that doesn't exist, creates the destination bucket for copying,
|
||||
copies the object, get's the object to check the length and content,
|
||||
get's the copied object to check the length and content, deletes all the objects inside the source bucket,
|
||||
deletes both the objects and buckets.`,
|
||||
Action: getAction(integration.TestPutGetObject),
|
||||
},
|
||||
{
|
||||
@@ -147,12 +160,127 @@ func initTestCommands() []*cli.Command {
|
||||
removes both the object and bucket`,
|
||||
Action: getAction(integration.TestInvalidMultiParts),
|
||||
},
|
||||
{
|
||||
Name: "object-tag-actions",
|
||||
Usage: "Tests get/put/delete object tag actions.",
|
||||
Description: `Creates a bucket with s3 gateway action, puts an object in it,
|
||||
puts some tags for the object, gets the tags, compares the results, removes the tags,
|
||||
gets the tags again, checks it to be empty, then removes both the object and bucket`,
|
||||
Action: getAction(integration.TestPutGetRemoveTags),
|
||||
},
|
||||
{
|
||||
Name: "bucket-acl-actions",
|
||||
Usage: "Tests put/get bucket actions.",
|
||||
Description: `Creates a bucket with s3 gateway action, puts some bucket acls
|
||||
gets the acl, verifies it, then removes the bucket`,
|
||||
Action: getAction(integration.TestAclActions),
|
||||
},
|
||||
{
|
||||
Name: "full-flow",
|
||||
Usage: "Tests the full flow of gateway.",
|
||||
Description: `Runs all the available tests to test the full flow of the gateway.`,
|
||||
Action: getAction(integration.TestFullFlow),
|
||||
},
|
||||
{
|
||||
Name: "bench",
|
||||
Usage: "Runs download/upload performance test on the gateway",
|
||||
Description: `Uploads/downloads some number(specified by flags) of files with some capacity(bytes).
|
||||
Logs the results to the console`,
|
||||
Flags: []cli.Flag{
|
||||
&cli.IntFlag{
|
||||
Name: "files",
|
||||
Usage: "Number of objects to read/write",
|
||||
Value: 1,
|
||||
Destination: &files,
|
||||
},
|
||||
&cli.Int64Flag{
|
||||
Name: "objsize",
|
||||
Usage: "Uploading object size",
|
||||
Value: 0,
|
||||
Destination: &objSize,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "prefix",
|
||||
Usage: "Object name prefix",
|
||||
Destination: &prefix,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "upload",
|
||||
Usage: "Upload data to the gateway",
|
||||
Value: false,
|
||||
Destination: &upload,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "download",
|
||||
Usage: "Download data to the gateway",
|
||||
Value: false,
|
||||
Destination: &download,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "bucket",
|
||||
Usage: "Destination bucket name to read/write data",
|
||||
Destination: &dstBucket,
|
||||
},
|
||||
&cli.Int64Flag{
|
||||
Name: "partSize",
|
||||
Usage: "Upload/download size per thread",
|
||||
Value: 64 * 1024 * 1024,
|
||||
Destination: &partSize,
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "concurrency",
|
||||
Usage: "Upload/download threads per object",
|
||||
Value: 1,
|
||||
Destination: &concurrency,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "pathStyle",
|
||||
Usage: "Use Pathstyle bucket addressing",
|
||||
Value: false,
|
||||
Destination: &pathStyle,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "checksumDis",
|
||||
Usage: "Disable server checksum",
|
||||
Value: false,
|
||||
Destination: &checksumDisable,
|
||||
},
|
||||
},
|
||||
Action: func(ctx *cli.Context) error {
|
||||
if upload && download {
|
||||
return fmt.Errorf("must only specify one of upload or download")
|
||||
}
|
||||
if !upload && !download {
|
||||
return fmt.Errorf("must specify one of upload or download")
|
||||
}
|
||||
|
||||
if dstBucket == "" {
|
||||
return fmt.Errorf("must specify bucket")
|
||||
}
|
||||
|
||||
opts := []integration.Option{
|
||||
integration.WithAccess(awsID),
|
||||
integration.WithSecret(awsSecret),
|
||||
integration.WithRegion(region),
|
||||
integration.WithEndpoint(endpoint),
|
||||
integration.WithConcurrency(concurrency),
|
||||
integration.WithPartSize(partSize),
|
||||
}
|
||||
if debug {
|
||||
opts = append(opts, integration.WithDebug())
|
||||
}
|
||||
if pathStyle {
|
||||
opts = append(opts, integration.WithPathStyle())
|
||||
}
|
||||
if checksumDisable {
|
||||
opts = append(opts, integration.WithDisableChecksum())
|
||||
}
|
||||
|
||||
s3conf := integration.NewS3Conf(opts...)
|
||||
|
||||
return integration.TestPerformance(s3conf, upload, download, files, objSize, dstBucket, prefix)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,6 +303,9 @@ func getAction(tf testFunc) func(*cli.Context) error {
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println("RAN:", integration.RunCount, "PASS:", integration.PassCount, "FAIL:", integration.FailCount)
|
||||
if integration.FailCount > 0 {
|
||||
return fmt.Errorf("test failed with %v errors", integration.FailCount)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
32
go.mod
32
go.mod
@@ -3,16 +3,17 @@ module github.com/versity/versitygw
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/Shopify/sarama v1.38.1
|
||||
github.com/aws/aws-sdk-go-v2 v1.18.1
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.35.0
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.36.0
|
||||
github.com/aws/smithy-go v1.13.5
|
||||
github.com/gofiber/fiber/v2 v2.46.0
|
||||
github.com/gofiber/fiber/v2 v2.47.0
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/pkg/xattr v0.4.9
|
||||
github.com/urfave/cli/v2 v2.25.6
|
||||
github.com/valyala/fasthttp v1.47.0
|
||||
github.com/urfave/cli/v2 v2.25.7
|
||||
github.com/valyala/fasthttp v1.48.0
|
||||
github.com/versity/scoutfs-go v0.0.0-20230606232754-0474b14343b9
|
||||
golang.org/x/sys v0.9.0
|
||||
golang.org/x/sys v0.10.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -21,7 +22,24 @@ require (
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.12.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.19.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/eapache/go-resiliency v1.3.0 // indirect
|
||||
github.com/eapache/go-xerial-snappy v0.0.0-20230111030713-bf00bc1b83b6 // indirect
|
||||
github.com/eapache/queue v1.1.0 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/hashicorp/errwrap v1.0.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/go-uuid v1.0.3 // indirect
|
||||
github.com/jcmturner/aescts/v2 v2.0.0 // indirect
|
||||
github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect
|
||||
github.com/jcmturner/gofork v1.7.6 // indirect
|
||||
github.com/jcmturner/gokrb5/v8 v8.4.3 // indirect
|
||||
github.com/jcmturner/rpc/v2 v2.0.3 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.17 // indirect
|
||||
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect
|
||||
golang.org/x/crypto v0.7.0 // indirect
|
||||
golang.org/x/net v0.8.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -29,7 +47,7 @@ require (
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.18.27
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.13.26
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.70
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.71
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.34 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.28 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.26 // indirect
|
||||
@@ -38,7 +56,7 @@ require (
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.28 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.3 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||
github.com/klauspost/compress v1.16.6 // indirect
|
||||
github.com/klauspost/compress v1.16.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.14 // indirect
|
||||
|
||||
92
go.sum
92
go.sum
@@ -1,3 +1,6 @@
|
||||
github.com/Shopify/sarama v1.38.1 h1:lqqPUPQZ7zPqYlWpTh+LQ9bhYNu2xJL6k1SJN4WVe2A=
|
||||
github.com/Shopify/sarama v1.38.1/go.mod h1:iwv9a67Ha8VNa+TifujYoWGxWnu2kNVAQdSdZ4X2o5g=
|
||||
github.com/Shopify/toxiproxy/v2 v2.5.0 h1:i4LPT+qrSlKNtQf5QliVjdP08GyAH8+BUIc9gT0eahc=
|
||||
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
|
||||
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/aws/aws-sdk-go-v2 v1.18.1 h1:+tefE750oAb7ZQGzla6bLkOwfcQCEtC5y2RqoqCeqKo=
|
||||
@@ -10,8 +13,8 @@ github.com/aws/aws-sdk-go-v2/credentials v1.13.26 h1:qmU+yhKmOCyujmuPY7tf5MxR/RK
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.13.26/go.mod h1:GoXt2YC8jHUBbA4jr+W3JiemnIbkXOfxSXcisUsZ3os=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.4 h1:LxK/bitrAr4lnh9LnIS6i7zWbCOdMsfzKFBI6LUCS0I=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.4/go.mod h1:E1hLXN/BL2e6YizK1zFlYd8vsfi2GTjbjBazinMmeaM=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.70 h1:4bh28MeeXoBFTjb0JjQ5sVatzlf5xA1DziV8mZed9v4=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.70/go.mod h1:9yI5NXzqy2yOiMytv6QLZHvlyHLwYxO9iIq+bZIbrFg=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.71 h1:SAB1UAVaf6nGCu3zyIrV+VWsendXrms1GqtW4zBotKA=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.71/go.mod h1:ZNo5H4PR3/fwsXYqb+Ld5YAfvHcYCbltaTTtSay4l2o=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.34 h1:A5UqQEmPaCFpedKouS4v+dHCTUo2sKqhoKO9U5kxyWo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.34/go.mod h1:wZpTEecJe0Btj3IYnDx/VlUzor9wm3fJHyvLpQF0VwY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.28 h1:srIVS45eQuewqz6fKKu6ZGXaq6FuFg5NzgQBAM6g8Y4=
|
||||
@@ -28,10 +31,8 @@ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.28 h1:bkRyG4a92
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.28/go.mod h1:jj7znCIg05jXlaGBlFMGP8+7UN3VtCkRBG2spnmRQkU=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.3 h1:dBL3StFxHtpBzJJ/mNEsjXVgfO+7jR0dAIEwLqMapEA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.3/go.mod h1:f1QyiAsvIv4B49DmCqrhlXqyaR+0IxMmyX+1P+AnzOM=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.34.1 h1:rYYwwsGqbwvGgQHjBkqgDt8MynXk+I8xgS0IEj5gOT0=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.34.1/go.mod h1:aVbf0sko/TsLWHx30c/uVu7c62+0EAJ3vbxaJga0xCw=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.35.0 h1:ya7fmrN2fE7s1P2gaPbNg5MTkERVWfsH8ToP1YC4Z9o=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.35.0/go.mod h1:aVbf0sko/TsLWHx30c/uVu7c62+0EAJ3vbxaJga0xCw=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.36.0 h1:lEmQ1XSD9qLk+NZXbgvLJI/IiTz7OIR2TYUTFH25EI4=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.36.0/go.mod h1:aVbf0sko/TsLWHx30c/uVu7c62+0EAJ3vbxaJga0xCw=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.12.12 h1:nneMBM2p79PGWBQovYO/6Xnc2ryRMw3InnDJq1FHkSY=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.12.12/go.mod h1:HuCOxYsF21eKrerARYO6HapNeh9GBNq7fius2AcwodY=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.12 h1:2qTR7IFk7/0IN/adSFhYu9Xthr0zVFTgBrmPldILn80=
|
||||
@@ -43,17 +44,50 @@ github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gofiber/fiber/v2 v2.46.0 h1:wkkWotblsGVlLjXj2dpgKQAYHtXumsK/HyFugQM68Ns=
|
||||
github.com/gofiber/fiber/v2 v2.46.0/go.mod h1:DNl0/c37WLe0g92U6lx1VMQuxGUQY5V7EIaVoEsUffc=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/eapache/go-resiliency v1.3.0 h1:RRL0nge+cWGlxXbUzJ7yMcq6w2XBEr19dCN6HECGaT0=
|
||||
github.com/eapache/go-resiliency v1.3.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho=
|
||||
github.com/eapache/go-xerial-snappy v0.0.0-20230111030713-bf00bc1b83b6 h1:8yY/I9ndfrgrXUbOGObLHKBR4Fl3nZXwM2c7OYTT8hM=
|
||||
github.com/eapache/go-xerial-snappy v0.0.0-20230111030713-bf00bc1b83b6/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0=
|
||||
github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc=
|
||||
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
||||
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
|
||||
github.com/gofiber/fiber/v2 v2.47.0 h1:EN5lHVCc+Pyqh5OEsk8fzRiifgwpbrP0rulQ4iNf3fs=
|
||||
github.com/gofiber/fiber/v2 v2.47.0/go.mod h1:mbFMVN1lQuzziTkkakgtKKdjfsXSw9BKR5lmcNksUoU=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
|
||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
|
||||
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
|
||||
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
|
||||
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
|
||||
github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=
|
||||
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
|
||||
github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=
|
||||
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
|
||||
github.com/jcmturner/gokrb5/v8 v8.4.3 h1:iTonLeSJOn7MVUtyMT+arAn5AKAPrkilzhGw8wE/Tq8=
|
||||
github.com/jcmturner/gokrb5/v8 v8.4.3/go.mod h1:dqRwJGXznQrzw6cWmyo6kH+E7jksEQG/CyVWsJEsJO0=
|
||||
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
|
||||
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
|
||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||
github.com/klauspost/compress v1.16.6 h1:91SKEy4K37vkp255cJ8QesJhjyRO0hn9i9G0GoUwLsk=
|
||||
github.com/klauspost/compress v1.16.6/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
|
||||
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
@@ -64,9 +98,14 @@ github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh
|
||||
github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
|
||||
github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
|
||||
github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
|
||||
github.com/pierrec/lz4/v4 v4.1.17 h1:kV4Ip+/hUBC+8T6+2EgburRtkE9ef4nbY3f4dFhGjMc=
|
||||
github.com/pierrec/lz4/v4 v4.1.17/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pkg/xattr v0.4.9 h1:5883YPCtkSd8LFbs13nXplj9g9tlrwoJRjgpgMu1/fE=
|
||||
github.com/pkg/xattr v0.4.9/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
||||
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
@@ -78,15 +117,20 @@ github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d/go.mod h1:Gy+0tqhJv
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk=
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/tinylib/msgp v1.1.6/go.mod h1:75BAfg2hauQhs3qedfdDZmWAPcFMAvJE5b9rGOMufyw=
|
||||
github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0=
|
||||
github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw=
|
||||
github.com/urfave/cli/v2 v2.25.6 h1:yuSkgDSZfH3L1CjF2/5fNNg2KbM47pY2EvjBq4ESQnU=
|
||||
github.com/urfave/cli/v2 v2.25.6/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
|
||||
github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs=
|
||||
github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.47.0 h1:y7moDoxYzMooFpT5aHgNgVOQDrS3qlkfiP9mDtGGK9c=
|
||||
github.com/valyala/fasthttp v1.47.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
|
||||
github.com/valyala/fasthttp v1.48.0 h1:oJWvHb9BIZToTQS3MuQ2R3bJZiNSa2KiNdeI8A+79Tc=
|
||||
github.com/valyala/fasthttp v1.48.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
|
||||
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||
github.com/versity/scoutfs-go v0.0.0-20230606232754-0474b14343b9 h1:ZfmQR01Kk6/kQh6+zlqfBYszVY02fzf9xYrchOY4NFM=
|
||||
@@ -99,23 +143,33 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.0.0-20220725212005-46097bf591d3/go.mod h1:AaygXjzTFtRAg2ttMY5RMuhpJ3cNnI0XpyFJD1iQRSM=
|
||||
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
||||
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -123,13 +177,14 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
|
||||
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -141,4 +196,9 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -38,6 +38,26 @@ func (r *RReader) Sum() []byte {
|
||||
return r.hash.Sum(nil)
|
||||
}
|
||||
|
||||
type ZReader struct {
|
||||
buf []byte
|
||||
dataleft int
|
||||
}
|
||||
|
||||
func NewZeroReader(totalsize, bufsize int) *ZReader {
|
||||
b := make([]byte, bufsize)
|
||||
return &ZReader{buf: b, dataleft: totalsize}
|
||||
}
|
||||
|
||||
func (r *ZReader) Read(p []byte) (int, error) {
|
||||
n := min(len(p), len(r.buf), r.dataleft)
|
||||
r.dataleft -= n
|
||||
err := error(nil)
|
||||
if n == 0 {
|
||||
err = io.EOF
|
||||
}
|
||||
return copy(p, r.buf[:n]), err
|
||||
}
|
||||
|
||||
func min(values ...int) int {
|
||||
if len(values) == 0 {
|
||||
return 0
|
||||
@@ -52,3 +72,13 @@ func min(values ...int) int {
|
||||
|
||||
return min
|
||||
}
|
||||
|
||||
type NW struct{}
|
||||
|
||||
func NewNullWriter() NW {
|
||||
return NW{}
|
||||
}
|
||||
|
||||
func (NW) WriteAt(p []byte, off int64) (n int, err error) {
|
||||
return len(p), nil
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -10,6 +11,8 @@ import (
|
||||
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/credentials"
|
||||
"github.com/aws/aws-sdk-go-v2/feature/s3/manager"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
"github.com/aws/smithy-go/middleware"
|
||||
)
|
||||
|
||||
@@ -26,10 +29,7 @@ type S3Conf struct {
|
||||
}
|
||||
|
||||
func NewS3Conf(opts ...Option) *S3Conf {
|
||||
s := &S3Conf{
|
||||
PartSize: 64 * 1024 * 1024, // 64B default chunksize
|
||||
Concurrency: 1, // 1 default concurrency
|
||||
}
|
||||
s := &S3Conf{}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(s)
|
||||
@@ -123,3 +123,31 @@ func (c *S3Conf) Config() aws.Config {
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
func (c *S3Conf) UploadData(r io.Reader, bucket, object string) error {
|
||||
uploader := manager.NewUploader(s3.NewFromConfig(c.Config()))
|
||||
uploader.PartSize = c.PartSize
|
||||
uploader.Concurrency = c.Concurrency
|
||||
|
||||
upinfo := &s3.PutObjectInput{
|
||||
Body: r,
|
||||
Bucket: &bucket,
|
||||
Key: &object,
|
||||
}
|
||||
|
||||
_, err := uploader.Upload(context.Background(), upinfo)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *S3Conf) DownloadData(w io.WriterAt, bucket, object string) (int64, error) {
|
||||
downloader := manager.NewDownloader(s3.NewFromConfig(c.Config()))
|
||||
downloader.PartSize = c.PartSize
|
||||
downloader.Concurrency = c.Concurrency
|
||||
|
||||
downinfo := &s3.GetObjectInput{
|
||||
Bucket: &bucket,
|
||||
Key: &object,
|
||||
}
|
||||
|
||||
return downloader.Download(context.Background(), w, downinfo)
|
||||
}
|
||||
|
||||
@@ -7,11 +7,12 @@ import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/feature/s3/manager"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
||||
)
|
||||
@@ -20,80 +21,35 @@ var (
|
||||
shortTimeout = 10 * time.Second
|
||||
)
|
||||
|
||||
func setup(s *S3Conf, bucket string) error {
|
||||
s3client := s3.NewFromConfig(s.Config())
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
_, err := s3client.CreateBucket(ctx, &s3.CreateBucketInput{
|
||||
Bucket: &bucket,
|
||||
})
|
||||
cancel()
|
||||
return err
|
||||
}
|
||||
|
||||
func teardown(s *S3Conf, bucket string) error {
|
||||
s3client := s3.NewFromConfig(s.Config())
|
||||
|
||||
deleteObject := func(bucket, key, versionId *string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
_, err := s3client.DeleteObject(ctx, &s3.DeleteObjectInput{
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
VersionId: versionId,
|
||||
})
|
||||
cancel()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete object %v: %v", *key, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
in := &s3.ListObjectsV2Input{Bucket: &bucket}
|
||||
for {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
out, err := s3client.ListObjectsV2(ctx, in)
|
||||
cancel()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list objects: %v", err)
|
||||
}
|
||||
|
||||
for _, item := range out.Contents {
|
||||
err = deleteObject(&bucket, item.Key, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if out.IsTruncated {
|
||||
in.ContinuationToken = out.ContinuationToken
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
_, err := s3client.DeleteBucket(ctx, &s3.DeleteBucketInput{
|
||||
Bucket: &bucket,
|
||||
})
|
||||
cancel()
|
||||
return err
|
||||
}
|
||||
|
||||
func TestMakeBucket(s *S3Conf) {
|
||||
testname := "test make bucket"
|
||||
testname := "test make/head/delete bucket"
|
||||
runF(testname)
|
||||
|
||||
s3client := s3.NewFromConfig(s.Config())
|
||||
|
||||
bucket := "testbucket"
|
||||
|
||||
err := setup(s, bucket)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
_, err := s3client.HeadBucket(ctx, &s3.HeadBucketInput{Bucket: &bucket})
|
||||
cancel()
|
||||
if err == nil {
|
||||
failF("%v: expected error, instead got success response", testname)
|
||||
return
|
||||
}
|
||||
|
||||
err = setup(s, bucket)
|
||||
if err != nil {
|
||||
failF("%v: %v", testname, err)
|
||||
return
|
||||
}
|
||||
passF(testname)
|
||||
|
||||
testname = "test delete empty bucket"
|
||||
runF(testname)
|
||||
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
|
||||
_, err = s3client.HeadBucket(ctx, &s3.HeadBucketInput{Bucket: &bucket})
|
||||
cancel()
|
||||
if err != nil {
|
||||
failF("%v: %v", testname, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = teardown(s, bucket)
|
||||
if err != nil {
|
||||
@@ -104,10 +60,16 @@ func TestMakeBucket(s *S3Conf) {
|
||||
}
|
||||
|
||||
func TestPutGetObject(s *S3Conf) {
|
||||
testname := "test put/get object"
|
||||
testname := "test put/get/delete/copy objects"
|
||||
runF(testname)
|
||||
|
||||
bucket := "testbucket1"
|
||||
dstBucket := "testdstbucket"
|
||||
obj := "myobject"
|
||||
obj2 := "myobject2"
|
||||
copySource := bucket + "/" + obj
|
||||
|
||||
s3client := s3.NewFromConfig(s.Config())
|
||||
|
||||
err := setup(s, bucket)
|
||||
if err != nil {
|
||||
@@ -122,13 +84,22 @@ func TestPutGetObject(s *S3Conf) {
|
||||
csum := sha256.Sum256(data)
|
||||
r := bytes.NewReader(data)
|
||||
|
||||
name := "myobject"
|
||||
s3client := s3.NewFromConfig(s.Config())
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
_, err = s3client.PutObject(ctx, &s3.PutObjectInput{
|
||||
Bucket: &bucket,
|
||||
Key: &name,
|
||||
Key: &obj,
|
||||
Body: r,
|
||||
})
|
||||
cancel()
|
||||
if err != nil {
|
||||
failF("%v: %v", testname, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
|
||||
_, err = s3client.PutObject(ctx, &s3.PutObjectInput{
|
||||
Bucket: &bucket,
|
||||
Key: &obj2,
|
||||
Body: r,
|
||||
})
|
||||
cancel()
|
||||
@@ -140,7 +111,7 @@ func TestPutGetObject(s *S3Conf) {
|
||||
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
|
||||
out, err := s3client.GetObject(ctx, &s3.GetObjectInput{
|
||||
Bucket: &bucket,
|
||||
Key: &name,
|
||||
Key: &obj,
|
||||
})
|
||||
defer cancel()
|
||||
if err != nil {
|
||||
@@ -166,11 +137,101 @@ func TestPutGetObject(s *S3Conf) {
|
||||
return
|
||||
}
|
||||
|
||||
// Expected error: destination bucket doesn't exist
|
||||
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
|
||||
_, err = s3client.CopyObject(ctx, &s3.CopyObjectInput{Bucket: &dstBucket, Key: &obj, CopySource: ©Source})
|
||||
cancel()
|
||||
if err == nil {
|
||||
failF("%v: expect bucket not found error instead got success response", testname)
|
||||
return
|
||||
}
|
||||
|
||||
err = setup(s, dstBucket)
|
||||
if err != nil {
|
||||
failF("%v: %v", testname, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
|
||||
_, err = s3client.CopyObject(ctx, &s3.CopyObjectInput{Bucket: &dstBucket, Key: &obj, CopySource: ©Source})
|
||||
cancel()
|
||||
if err != nil {
|
||||
failF("%v: %v", testname, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
|
||||
copyObjOut, err := s3client.GetObject(ctx, &s3.GetObjectInput{
|
||||
Bucket: &dstBucket,
|
||||
Key: &obj,
|
||||
})
|
||||
defer cancel()
|
||||
if err != nil {
|
||||
failF("%v: %v", testname, err)
|
||||
return
|
||||
}
|
||||
defer copyObjOut.Body.Close()
|
||||
|
||||
if copyObjOut.ContentLength != int64(datalen) {
|
||||
failF("%v: content length got %v expected %v", testname, copyObjOut.ContentLength, datalen)
|
||||
return
|
||||
}
|
||||
|
||||
b, err = io.ReadAll(copyObjOut.Body)
|
||||
if err != nil {
|
||||
failF("%v: read body %v", testname, err)
|
||||
return
|
||||
}
|
||||
|
||||
copysum := sha256.Sum256(b)
|
||||
if csum != copysum {
|
||||
failF("%v: copied object checksum got %x expected %x", testname, copysum, csum)
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
|
||||
_, err = s3client.DeleteObjects(ctx, &s3.DeleteObjectsInput{Bucket: &bucket, Delete: &types.Delete{Objects: []types.ObjectIdentifier{{Key: &obj}, {Key: &obj2}}}})
|
||||
cancel()
|
||||
if err != nil {
|
||||
failF("%v: %v", testname, err)
|
||||
return
|
||||
}
|
||||
|
||||
objCount := 0
|
||||
|
||||
in := &s3.ListObjectsV2Input{Bucket: &bucket}
|
||||
for {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
out, err := s3client.ListObjectsV2(ctx, in)
|
||||
cancel()
|
||||
if err != nil {
|
||||
failF("%v: %v", testname, err)
|
||||
return
|
||||
}
|
||||
objCount += len(out.Contents)
|
||||
if out.IsTruncated {
|
||||
in.ContinuationToken = out.ContinuationToken
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if objCount != 2 {
|
||||
failF("%v: expected object count %v instead got %v", testname, 2, objCount)
|
||||
}
|
||||
|
||||
err = teardown(s, bucket)
|
||||
if err != nil {
|
||||
failF("%v: %v", testname, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = teardown(s, dstBucket)
|
||||
if err != nil {
|
||||
failF("%v: %v", testname, err)
|
||||
return
|
||||
}
|
||||
|
||||
passF(testname)
|
||||
}
|
||||
|
||||
@@ -193,7 +254,7 @@ func TestPutGetMPObject(s *S3Conf) {
|
||||
dr := NewDataReader(datalen, 5*1024*1024)
|
||||
WithPartSize(5 * 1024 * 1024)
|
||||
s.PartSize = 5 * 1024 * 1024
|
||||
err = uploadData(s, dr, bucket, name)
|
||||
err = s.UploadData(dr, bucket, name)
|
||||
if err != nil {
|
||||
failF("%v: %v", testname, err)
|
||||
return
|
||||
@@ -244,35 +305,6 @@ func TestPutGetMPObject(s *S3Conf) {
|
||||
passF(testname)
|
||||
}
|
||||
|
||||
func isEqual(a, b []byte) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i, d := range a {
|
||||
if d != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func uploadData(s *S3Conf, r io.Reader, bucket, object string) error {
|
||||
uploader := manager.NewUploader(s3.NewFromConfig(s.Config()))
|
||||
uploader.PartSize = s.PartSize
|
||||
uploader.Concurrency = s.Concurrency
|
||||
|
||||
upinfo := &s3.PutObjectInput{
|
||||
Body: r,
|
||||
Bucket: &bucket,
|
||||
Key: &object,
|
||||
}
|
||||
|
||||
_, err := uploader.Upload(context.Background(), upinfo)
|
||||
return err
|
||||
}
|
||||
|
||||
func TestPutDirObject(s *S3Conf) {
|
||||
testname := "test put directory object"
|
||||
runF(testname)
|
||||
@@ -448,16 +480,6 @@ func TestListObject(s *S3Conf) {
|
||||
passF(testname)
|
||||
}
|
||||
|
||||
func contains(name string, list []types.Object) bool {
|
||||
for _, item := range list {
|
||||
fmt.Println(*item.Key)
|
||||
if strings.EqualFold(name, *item.Key) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func TestListAbortMultiPartObject(s *S3Conf) {
|
||||
testname := "list/abort multipart objects"
|
||||
runF(testname)
|
||||
@@ -542,15 +564,6 @@ func TestListAbortMultiPartObject(s *S3Conf) {
|
||||
passF(testname)
|
||||
}
|
||||
|
||||
func containsUID(name, id string, list []types.MultipartUpload) bool {
|
||||
for _, item := range list {
|
||||
if strings.EqualFold(name, *item.Key) && strings.EqualFold(id, *item.UploadId) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func TestListMultiParts(s *S3Conf) {
|
||||
testname := "list multipart parts"
|
||||
runF(testname)
|
||||
@@ -921,15 +934,6 @@ func TestIncompleteMultiParts(s *S3Conf) {
|
||||
passF(testname)
|
||||
}
|
||||
|
||||
func containsPart(part int32, list []types.Part) bool {
|
||||
for _, item := range list {
|
||||
if item.PartNumber == part {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func TestIncompletePutObject(s *S3Conf) {
|
||||
testname := "test incomplete put object"
|
||||
runF(testname)
|
||||
@@ -1038,7 +1042,34 @@ func TestRangeGet(s *S3Conf) {
|
||||
}
|
||||
|
||||
// bytes range is inclusive, go range for second value is not
|
||||
if !isSame(b, data[100:201]) {
|
||||
if !isEqual(b, data[100:201]) {
|
||||
failF("%v: data mismatch of range", testname)
|
||||
return
|
||||
}
|
||||
|
||||
rangeString = "bytes=100-"
|
||||
|
||||
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
|
||||
out, err = s3client.GetObject(ctx, &s3.GetObjectInput{
|
||||
Bucket: &bucket,
|
||||
Key: &name,
|
||||
Range: &rangeString,
|
||||
})
|
||||
defer cancel()
|
||||
if err != nil {
|
||||
failF("%v: %v", testname, err)
|
||||
return
|
||||
}
|
||||
defer out.Body.Close()
|
||||
|
||||
b, err = io.ReadAll(out.Body)
|
||||
if err != nil {
|
||||
failF("%v: read body %v", testname, err)
|
||||
return
|
||||
}
|
||||
|
||||
// bytes range is inclusive, go range for second value is not
|
||||
if !isEqual(b, data[100:]) {
|
||||
failF("%v: data mismatch of range", testname)
|
||||
return
|
||||
}
|
||||
@@ -1051,18 +1082,6 @@ func TestRangeGet(s *S3Conf) {
|
||||
passF(testname)
|
||||
}
|
||||
|
||||
func isSame(a, b []byte) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i, x := range a {
|
||||
if x != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func TestInvalidMultiParts(s *S3Conf) {
|
||||
testname := "invalid multipart parts"
|
||||
runF(testname)
|
||||
@@ -1148,6 +1167,288 @@ func TestInvalidMultiParts(s *S3Conf) {
|
||||
passF(testname)
|
||||
}
|
||||
|
||||
type prefResult struct {
|
||||
elapsed time.Duration
|
||||
size int64
|
||||
err error
|
||||
}
|
||||
|
||||
func TestPerformance(s *S3Conf, upload, download bool, files int, objectSize int64, bucket, prefix string) error {
|
||||
var sg sync.WaitGroup
|
||||
results := make([]prefResult, files)
|
||||
start := time.Now()
|
||||
if upload {
|
||||
if objectSize == 0 {
|
||||
return fmt.Errorf("must specify object size for upload")
|
||||
}
|
||||
|
||||
if objectSize > (int64(10000) * s.PartSize) {
|
||||
return fmt.Errorf("object size can not exceed 10000 * chunksize")
|
||||
}
|
||||
|
||||
runF("performance test: upload/download objects")
|
||||
|
||||
for i := 0; i < files; i++ {
|
||||
sg.Add(1)
|
||||
go func(i int) {
|
||||
var r io.Reader = NewDataReader(int(objectSize), int(s.PartSize))
|
||||
|
||||
start := time.Now()
|
||||
err := s.UploadData(r, bucket, fmt.Sprintf("%v%v", prefix, i))
|
||||
results[i].elapsed = time.Since(start)
|
||||
results[i].err = err
|
||||
results[i].size = objectSize
|
||||
sg.Done()
|
||||
}(i)
|
||||
}
|
||||
}
|
||||
if download {
|
||||
for i := 0; i < files; i++ {
|
||||
sg.Add(1)
|
||||
go func(i int) {
|
||||
nw := NewNullWriter()
|
||||
start := time.Now()
|
||||
n, err := s.DownloadData(nw, bucket, fmt.Sprintf("%v%v", prefix, i))
|
||||
results[i].elapsed = time.Since(start)
|
||||
results[i].err = err
|
||||
results[i].size = n
|
||||
sg.Done()
|
||||
}(i)
|
||||
}
|
||||
}
|
||||
sg.Wait()
|
||||
elapsed := time.Since(start)
|
||||
|
||||
var tot int64
|
||||
for i, res := range results {
|
||||
if res.err != nil {
|
||||
failF("%v: %v\n", i, res.err)
|
||||
break
|
||||
}
|
||||
tot += res.size
|
||||
fmt.Printf("%v: %v in %v (%v MB/s)\n",
|
||||
i, res.size, res.elapsed,
|
||||
int(math.Ceil(float64(res.size)/res.elapsed.Seconds())/1048576))
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
passF("run perf: %v in %v (%v MB/s)\n",
|
||||
tot, elapsed, int(math.Ceil(float64(tot)/elapsed.Seconds())/1048576))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestPutGetRemoveTags(s *S3Conf) {
|
||||
testname := "test put/get/remove object tags"
|
||||
runF(testname)
|
||||
|
||||
bucket := "testbucket13"
|
||||
|
||||
err := setup(s, bucket)
|
||||
if err != nil {
|
||||
failF("%v: %v", testname, err)
|
||||
return
|
||||
}
|
||||
|
||||
obj := "myobject"
|
||||
s3client := s3.NewFromConfig(s.Config())
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
_, err = s3client.PutObject(ctx, &s3.PutObjectInput{
|
||||
Bucket: &bucket,
|
||||
Key: &obj,
|
||||
})
|
||||
cancel()
|
||||
if err != nil {
|
||||
failF("%v: %v", testname, err)
|
||||
return
|
||||
}
|
||||
|
||||
key1 := "hello1"
|
||||
key2 := "hello2"
|
||||
val1 := "world1"
|
||||
val2 := "world2"
|
||||
|
||||
tagging := types.Tagging{TagSet: []types.Tag{{Key: &key1, Value: &val1}, {Key: &key2, Value: &val2}}}
|
||||
|
||||
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
|
||||
_, err = s3client.PutObjectTagging(ctx, &s3.PutObjectTaggingInput{
|
||||
Bucket: &bucket,
|
||||
Key: &obj,
|
||||
Tagging: &tagging,
|
||||
})
|
||||
cancel()
|
||||
if err != nil {
|
||||
failF("%v: %v", testname, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
|
||||
out, err := s3client.GetObjectTagging(ctx, &s3.GetObjectTaggingInput{
|
||||
Key: &obj,
|
||||
Bucket: &bucket,
|
||||
})
|
||||
cancel()
|
||||
if err != nil {
|
||||
failF("%v: %v", testname, err)
|
||||
return
|
||||
}
|
||||
|
||||
ok := areTagsSame(tagging.TagSet, out.TagSet)
|
||||
if !ok {
|
||||
failF("%v: expected %v instead got %v", testname, tagging.TagSet, out.TagSet)
|
||||
}
|
||||
|
||||
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
|
||||
_, err = s3client.DeleteObjectTagging(ctx, &s3.DeleteObjectTaggingInput{
|
||||
Key: &obj,
|
||||
Bucket: &bucket,
|
||||
})
|
||||
cancel()
|
||||
if err != nil {
|
||||
failF("%v: %v", testname, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
|
||||
out, err = s3client.GetObjectTagging(ctx, &s3.GetObjectTaggingInput{
|
||||
Key: &obj,
|
||||
Bucket: &bucket,
|
||||
})
|
||||
cancel()
|
||||
if err != nil {
|
||||
failF("%v: %v", testname, err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(out.TagSet) > 0 {
|
||||
failF("%v: expected empty tag set instead got %v", testname, out.TagSet)
|
||||
}
|
||||
|
||||
err = teardown(s, bucket)
|
||||
if err != nil {
|
||||
failF("%v: %v", testname, err)
|
||||
return
|
||||
}
|
||||
passF(testname)
|
||||
}
|
||||
|
||||
func TestAclActions(s *S3Conf) {
|
||||
testname := "test put/get acl"
|
||||
runF(testname)
|
||||
|
||||
bucket := "testbucket14"
|
||||
|
||||
err := setup(s, bucket)
|
||||
if err != nil {
|
||||
failF("%v: %v", testname, err)
|
||||
return
|
||||
}
|
||||
|
||||
s3client := s3.NewFromConfig(s.Config())
|
||||
|
||||
rootAccess := s.awsID
|
||||
rootSecret := s.awsSecret
|
||||
|
||||
s.awsID = "grt1"
|
||||
s.awsSecret = "grt1secret"
|
||||
|
||||
userS3Client := s3.NewFromConfig(s.Config())
|
||||
|
||||
s.awsID = rootAccess
|
||||
s.awsSecret = rootSecret
|
||||
|
||||
grt1 := "grt1"
|
||||
|
||||
grants := []types.Grant{
|
||||
{
|
||||
Permission: "READ",
|
||||
Grantee: &types.Grantee{
|
||||
ID: &grt1,
|
||||
Type: "CanonicalUser",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
succUsrCrt := "The user has been created successfully"
|
||||
failUsrCrt := "failed to create a user: update iam data: account already exists"
|
||||
|
||||
out, err := execCommand("admin", "-a", s.awsID, "-s", s.awsSecret, "create-user", "-a", grt1, "-s", "grt1secret", "-r", "user")
|
||||
if err != nil {
|
||||
failF("%v: %v", err)
|
||||
return
|
||||
}
|
||||
if !strings.Contains(string(out), succUsrCrt) && !strings.Contains(string(out), failUsrCrt) {
|
||||
failF("%v: failed to create user accounts", testname)
|
||||
return
|
||||
}
|
||||
|
||||
// Validation error case
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
_, err = s3client.PutBucketAcl(ctx, &s3.PutBucketAclInput{
|
||||
Bucket: &bucket,
|
||||
AccessControlPolicy: &types.AccessControlPolicy{
|
||||
Grants: grants,
|
||||
},
|
||||
ACL: "private",
|
||||
})
|
||||
cancel()
|
||||
if err == nil {
|
||||
failF("%v: expected validation error", testname)
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
|
||||
_, err = s3client.PutBucketAcl(ctx, &s3.PutBucketAclInput{
|
||||
Bucket: &bucket,
|
||||
AccessControlPolicy: &types.AccessControlPolicy{
|
||||
Grants: grants,
|
||||
Owner: &types.Owner{ID: &s.awsID},
|
||||
},
|
||||
})
|
||||
cancel()
|
||||
if err != nil {
|
||||
failF("%v: %v", testname, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
|
||||
acl, err := s3client.GetBucketAcl(ctx, &s3.GetBucketAclInput{
|
||||
Bucket: &bucket,
|
||||
})
|
||||
cancel()
|
||||
if err != nil {
|
||||
failF("%v: %v", testname, err)
|
||||
return
|
||||
}
|
||||
|
||||
if *acl.Owner.ID != s.awsID {
|
||||
failF("%v: expected bucket owner: %v, instead got: %v", testname, s.awsID, *acl.Owner.ID)
|
||||
return
|
||||
}
|
||||
if !checkGrants(acl.Grants, grants) {
|
||||
failF("%v: expected %v, instead got %v", testname, grants, acl.Grants)
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
|
||||
_, err = userS3Client.PutBucketAcl(ctx, &s3.PutBucketAclInput{
|
||||
Bucket: &bucket,
|
||||
})
|
||||
cancel()
|
||||
if err == nil {
|
||||
failF("%v: expected acl access denied error", testname)
|
||||
return
|
||||
}
|
||||
|
||||
err = teardown(s, bucket)
|
||||
if err != nil {
|
||||
failF("%v: %v", testname, err)
|
||||
return
|
||||
}
|
||||
passF(testname)
|
||||
}
|
||||
|
||||
// Full flow test
|
||||
func TestFullFlow(s *S3Conf) {
|
||||
// TODO: add more test cases to get 100% coverage
|
||||
@@ -1161,6 +1462,8 @@ func TestFullFlow(s *S3Conf) {
|
||||
TestIncompleteMultiParts(s)
|
||||
TestIncorrectMultiParts(s)
|
||||
TestListAbortMultiPartObject(s)
|
||||
TestListAbortMultiPartObject(s)
|
||||
TestRangeGet(s)
|
||||
TestInvalidMultiParts(s)
|
||||
TestPutGetRemoveTags(s)
|
||||
TestAclActions(s)
|
||||
}
|
||||
|
||||
156
integration/utils.go
Normal file
156
integration/utils.go
Normal file
@@ -0,0 +1,156 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
||||
)
|
||||
|
||||
func setup(s *S3Conf, bucket string) error {
|
||||
s3client := s3.NewFromConfig(s.Config())
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
_, err := s3client.CreateBucket(ctx, &s3.CreateBucketInput{
|
||||
Bucket: &bucket,
|
||||
})
|
||||
cancel()
|
||||
return err
|
||||
}
|
||||
|
||||
func teardown(s *S3Conf, bucket string) error {
|
||||
s3client := s3.NewFromConfig(s.Config())
|
||||
|
||||
deleteObject := func(bucket, key, versionId *string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
_, err := s3client.DeleteObject(ctx, &s3.DeleteObjectInput{
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
VersionId: versionId,
|
||||
})
|
||||
cancel()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete object %v: %v", *key, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
in := &s3.ListObjectsV2Input{Bucket: &bucket}
|
||||
for {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
out, err := s3client.ListObjectsV2(ctx, in)
|
||||
cancel()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list objects: %v", err)
|
||||
}
|
||||
|
||||
for _, item := range out.Contents {
|
||||
err = deleteObject(&bucket, item.Key, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if out.IsTruncated {
|
||||
in.ContinuationToken = out.ContinuationToken
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
_, err := s3client.DeleteBucket(ctx, &s3.DeleteBucketInput{
|
||||
Bucket: &bucket,
|
||||
})
|
||||
cancel()
|
||||
return err
|
||||
}
|
||||
|
||||
func isEqual(a, b []byte) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i, d := range a {
|
||||
if d != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func contains(name string, list []types.Object) bool {
|
||||
for _, item := range list {
|
||||
fmt.Println(*item.Key)
|
||||
if strings.EqualFold(name, *item.Key) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func containsUID(name, id string, list []types.MultipartUpload) bool {
|
||||
for _, item := range list {
|
||||
if strings.EqualFold(name, *item.Key) && strings.EqualFold(id, *item.UploadId) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func containsPart(part int32, list []types.Part) bool {
|
||||
for _, item := range list {
|
||||
if item.PartNumber == part {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func areTagsSame(tags1, tags2 []types.Tag) bool {
|
||||
if len(tags1) != len(tags2) {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, tag := range tags1 {
|
||||
if !containsTag(tag, tags2) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func containsTag(tag types.Tag, list []types.Tag) bool {
|
||||
for _, item := range list {
|
||||
if *item.Key == *tag.Key && *item.Value == *tag.Value {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func checkGrants(grts1, grts2 []types.Grant) bool {
|
||||
if len(grts1) != len(grts2) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i, grt := range grts1 {
|
||||
if grt.Permission != grts2[i].Permission {
|
||||
return false
|
||||
}
|
||||
if *grt.Grantee.ID != *grts2[i].Grantee.ID {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func execCommand(args ...string) ([]byte, error) {
|
||||
cmd := exec.Command("./versitygw", args...)
|
||||
|
||||
return cmd.CombinedOutput()
|
||||
}
|
||||
37
runtests.sh
Executable file
37
runtests.sh
Executable file
@@ -0,0 +1,37 @@
|
||||
#!/bin/bash
|
||||
|
||||
# make temp dirs
|
||||
mkdir /tmp/gw
|
||||
rm -rf /tmp/covdata
|
||||
mkdir /tmp/covdata
|
||||
|
||||
# run server in background
|
||||
GOCOVERDIR=/tmp/covdata ./versitygw -a user -s pass posix /tmp/gw &
|
||||
GW_PID=$!
|
||||
|
||||
# wait a second for server to start up
|
||||
sleep 1
|
||||
|
||||
# check if server is still running
|
||||
if ! kill -0 $GW_PID; then
|
||||
echo "server no longer running"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# run tests
|
||||
if ! ./versitygw test -a user -s pass -e http://127.0.0.1:7070 full-flow; then
|
||||
echo "tests failed"
|
||||
kill $GW_PID
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# kill off server
|
||||
kill $GW_PID
|
||||
exit 0
|
||||
|
||||
# if the above binary was built with -cover enabled (make testbin),
|
||||
# then the following can be used for code coverage reports:
|
||||
# go tool covdata percent -i=/tmp/covdata
|
||||
# go tool covdata textfmt -i=/tmp/covdata -o profile.txt
|
||||
# go tool cover -html=profile.txt
|
||||
|
||||
@@ -27,11 +27,14 @@ type AdminController struct {
|
||||
|
||||
func (c AdminController) CreateUser(ctx *fiber.Ctx) error {
|
||||
access, secret, role := ctx.Query("access"), ctx.Query("secret"), ctx.Query("role")
|
||||
requesterRole := ctx.Locals("role")
|
||||
requesterRole := ctx.Locals("role").(string)
|
||||
|
||||
if requesterRole != "admin" {
|
||||
return fmt.Errorf("access denied: only admin users have access to this resource")
|
||||
}
|
||||
if role != "user" && role != "admin" {
|
||||
return fmt.Errorf("invalid parameters: user role have to be one of the following: 'user', 'admin'")
|
||||
}
|
||||
|
||||
user := auth.Account{Secret: secret, Role: role}
|
||||
|
||||
@@ -40,13 +43,12 @@ func (c AdminController) CreateUser(ctx *fiber.Ctx) error {
|
||||
return fmt.Errorf("failed to create a user: %w", err)
|
||||
}
|
||||
|
||||
ctx.SendString("The user has been created successfully")
|
||||
return nil
|
||||
return ctx.SendString("The user has been created successfully")
|
||||
}
|
||||
|
||||
func (c AdminController) DeleteUser(ctx *fiber.Ctx) error {
|
||||
access := ctx.Query("access")
|
||||
requesterRole := ctx.Locals("role")
|
||||
requesterRole := ctx.Locals("role").(string)
|
||||
if requesterRole != "admin" {
|
||||
return fmt.Errorf("access denied: only admin users have access to this resource")
|
||||
}
|
||||
@@ -56,6 +58,5 @@ func (c AdminController) DeleteUser(ctx *fiber.Ctx) error {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx.SendString("The user has been created successfully")
|
||||
return nil
|
||||
return ctx.SendString("The user has been deleted successfully")
|
||||
}
|
||||
|
||||
173
s3api/controllers/admin_test.go
Normal file
173
s3api/controllers/admin_test.go
Normal file
@@ -0,0 +1,173 @@
|
||||
// Copyright 2023 Versity Software
|
||||
// This file is licensed under the Apache License, Version 2.0
|
||||
// (the "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/versity/versitygw/auth"
|
||||
)
|
||||
|
||||
func TestAdminController_CreateUser(t *testing.T) {
|
||||
type args struct {
|
||||
req *http.Request
|
||||
}
|
||||
|
||||
adminController := AdminController{
|
||||
IAMService: &IAMServiceMock{
|
||||
CreateAccountFunc: func(access string, account auth.Account) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
app := fiber.New()
|
||||
|
||||
app.Use(func(ctx *fiber.Ctx) error {
|
||||
ctx.Locals("role", "admin")
|
||||
return ctx.Next()
|
||||
})
|
||||
|
||||
app.Patch("/create-user", adminController.CreateUser)
|
||||
|
||||
appErr := fiber.New()
|
||||
|
||||
appErr.Use(func(ctx *fiber.Ctx) error {
|
||||
ctx.Locals("role", "user")
|
||||
return ctx.Next()
|
||||
})
|
||||
|
||||
appErr.Patch("/create-user", adminController.CreateUser)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
app *fiber.App
|
||||
args args
|
||||
wantErr bool
|
||||
statusCode int
|
||||
}{
|
||||
{
|
||||
name: "Admin-create-user-success",
|
||||
app: app,
|
||||
args: args{
|
||||
req: httptest.NewRequest(http.MethodPatch, "/create-user?access=test&secret=test&role=user", nil),
|
||||
},
|
||||
wantErr: false,
|
||||
statusCode: 200,
|
||||
},
|
||||
{
|
||||
name: "Admin-create-user-invalid-user-role",
|
||||
app: app,
|
||||
args: args{
|
||||
req: httptest.NewRequest(http.MethodPatch, "/create-user?access=test&secret=test&role=invalid", nil),
|
||||
},
|
||||
wantErr: false,
|
||||
statusCode: 500,
|
||||
},
|
||||
{
|
||||
name: "Admin-create-user-invalid-requester-role",
|
||||
app: appErr,
|
||||
args: args{
|
||||
req: httptest.NewRequest(http.MethodPatch, "/create-user?access=test&secret=test&role=admin", nil),
|
||||
},
|
||||
wantErr: false,
|
||||
statusCode: 500,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
resp, err := tt.app.Test(tt.args.req)
|
||||
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("AdminController.CreateUser() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
|
||||
if resp.StatusCode != tt.statusCode {
|
||||
t.Errorf("AdminController.CreateUser() statusCode = %v, wantStatusCode = %v", resp.StatusCode, tt.statusCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminController_DeleteUser(t *testing.T) {
|
||||
type args struct {
|
||||
req *http.Request
|
||||
}
|
||||
|
||||
adminController := AdminController{
|
||||
IAMService: &IAMServiceMock{
|
||||
DeleteUserAccountFunc: func(access string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
app := fiber.New()
|
||||
|
||||
app.Use(func(ctx *fiber.Ctx) error {
|
||||
ctx.Locals("role", "admin")
|
||||
return ctx.Next()
|
||||
})
|
||||
|
||||
app.Patch("/delete-user", adminController.DeleteUser)
|
||||
|
||||
appErr := fiber.New()
|
||||
|
||||
appErr.Use(func(ctx *fiber.Ctx) error {
|
||||
ctx.Locals("role", "user")
|
||||
return ctx.Next()
|
||||
})
|
||||
|
||||
appErr.Patch("/delete-user", adminController.DeleteUser)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
app *fiber.App
|
||||
args args
|
||||
wantErr bool
|
||||
statusCode int
|
||||
}{
|
||||
{
|
||||
name: "Admin-delete-user-success",
|
||||
app: app,
|
||||
args: args{
|
||||
req: httptest.NewRequest(http.MethodPatch, "/delete-user?access=test", nil),
|
||||
},
|
||||
wantErr: false,
|
||||
statusCode: 200,
|
||||
},
|
||||
{
|
||||
name: "Admin-delete-user-invalid-requester-role",
|
||||
app: appErr,
|
||||
args: args{
|
||||
req: httptest.NewRequest(http.MethodPatch, "/delete-user?access=test", nil),
|
||||
},
|
||||
wantErr: false,
|
||||
statusCode: 500,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
resp, err := tt.app.Test(tt.args.req)
|
||||
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("AdminController.DeleteUser() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
|
||||
if resp.StatusCode != tt.statusCode {
|
||||
t.Errorf("AdminController.DeleteUser() statusCode = %v, wantStatusCode = %v", resp.StatusCode, tt.statusCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,12 +28,9 @@ var _ backend.Backend = &BackendMock{}
|
||||
// CompleteMultipartUploadFunc: func(bucket string, object string, uploadID string, parts []types.Part) (*s3.CompleteMultipartUploadOutput, error) {
|
||||
// panic("mock out the CompleteMultipartUpload method")
|
||||
// },
|
||||
// CopyObjectFunc: func(srcBucket string, srcObject string, DstBucket string, dstObject string) (*s3.CopyObjectOutput, error) {
|
||||
// CopyObjectFunc: func(srcBucket string, srcObject string, dstBucket string, dstObject string) (*s3.CopyObjectOutput, error) {
|
||||
// panic("mock out the CopyObject method")
|
||||
// },
|
||||
// CopyPartFunc: func(srcBucket string, srcObject string, DstBucket string, uploadID string, rangeHeader string, part int) (*types.CopyPartResult, error) {
|
||||
// panic("mock out the CopyPart method")
|
||||
// },
|
||||
// CreateMultipartUploadFunc: func(createMultipartUploadInput *s3.CreateMultipartUploadInput) (*s3.CreateMultipartUploadOutput, error) {
|
||||
// panic("mock out the CreateMultipartUpload method")
|
||||
// },
|
||||
@@ -70,7 +67,7 @@ var _ backend.Backend = &BackendMock{}
|
||||
// ListBucketsFunc: func() (s3response.ListAllMyBucketsResult, error) {
|
||||
// panic("mock out the ListBuckets method")
|
||||
// },
|
||||
// ListMultipartUploadsFunc: func(output *s3.ListMultipartUploadsInput) (s3response.ListMultipartUploadsResponse, error) {
|
||||
// ListMultipartUploadsFunc: func(listMultipartUploadsInput *s3.ListMultipartUploadsInput) (s3response.ListMultipartUploadsResponse, error) {
|
||||
// panic("mock out the ListMultipartUploads method")
|
||||
// },
|
||||
// ListObjectPartsFunc: func(bucket string, object string, uploadID string, partNumberMarker int, maxParts int) (s3response.ListPartsResponse, error) {
|
||||
@@ -112,7 +109,7 @@ var _ backend.Backend = &BackendMock{}
|
||||
// StringFunc: func() string {
|
||||
// panic("mock out the String method")
|
||||
// },
|
||||
// UploadPartCopyFunc: func(uploadPartCopyInput *s3.UploadPartCopyInput) (*s3.UploadPartCopyOutput, error) {
|
||||
// UploadPartCopyFunc: func(uploadPartCopyInput *s3.UploadPartCopyInput) (s3response.CopyObjectResult, error) {
|
||||
// panic("mock out the UploadPartCopy method")
|
||||
// },
|
||||
// }
|
||||
@@ -129,10 +126,7 @@ type BackendMock struct {
|
||||
CompleteMultipartUploadFunc func(bucket string, object string, uploadID string, parts []types.Part) (*s3.CompleteMultipartUploadOutput, error)
|
||||
|
||||
// CopyObjectFunc mocks the CopyObject method.
|
||||
CopyObjectFunc func(srcBucket string, srcObject string, DstBucket string, dstObject string) (*s3.CopyObjectOutput, error)
|
||||
|
||||
// CopyPartFunc mocks the CopyPart method.
|
||||
CopyPartFunc func(srcBucket string, srcObject string, DstBucket string, uploadID string, rangeHeader string, part int) (*types.CopyPartResult, error)
|
||||
CopyObjectFunc func(srcBucket string, srcObject string, dstBucket string, dstObject string) (*s3.CopyObjectOutput, error)
|
||||
|
||||
// CreateMultipartUploadFunc mocks the CreateMultipartUpload method.
|
||||
CreateMultipartUploadFunc func(createMultipartUploadInput *s3.CreateMultipartUploadInput) (*s3.CreateMultipartUploadOutput, error)
|
||||
@@ -171,7 +165,7 @@ type BackendMock struct {
|
||||
ListBucketsFunc func() (s3response.ListAllMyBucketsResult, error)
|
||||
|
||||
// ListMultipartUploadsFunc mocks the ListMultipartUploads method.
|
||||
ListMultipartUploadsFunc func(output *s3.ListMultipartUploadsInput) (s3response.ListMultipartUploadsResponse, error)
|
||||
ListMultipartUploadsFunc func(listMultipartUploadsInput *s3.ListMultipartUploadsInput) (s3response.ListMultipartUploadsResponse, error)
|
||||
|
||||
// ListObjectPartsFunc mocks the ListObjectParts method.
|
||||
ListObjectPartsFunc func(bucket string, object string, uploadID string, partNumberMarker int, maxParts int) (s3response.ListPartsResponse, error)
|
||||
@@ -213,7 +207,7 @@ type BackendMock struct {
|
||||
StringFunc func() string
|
||||
|
||||
// UploadPartCopyFunc mocks the UploadPartCopy method.
|
||||
UploadPartCopyFunc func(uploadPartCopyInput *s3.UploadPartCopyInput) (*s3.UploadPartCopyOutput, error)
|
||||
UploadPartCopyFunc func(uploadPartCopyInput *s3.UploadPartCopyInput) (s3response.CopyObjectResult, error)
|
||||
|
||||
// calls tracks calls to the methods.
|
||||
calls struct {
|
||||
@@ -239,26 +233,11 @@ type BackendMock struct {
|
||||
SrcBucket string
|
||||
// SrcObject is the srcObject argument value.
|
||||
SrcObject string
|
||||
// DstBucket is the DstBucket argument value.
|
||||
// DstBucket is the dstBucket argument value.
|
||||
DstBucket string
|
||||
// DstObject is the dstObject argument value.
|
||||
DstObject string
|
||||
}
|
||||
// CopyPart holds details about calls to the CopyPart method.
|
||||
CopyPart []struct {
|
||||
// SrcBucket is the srcBucket argument value.
|
||||
SrcBucket string
|
||||
// SrcObject is the srcObject argument value.
|
||||
SrcObject string
|
||||
// DstBucket is the DstBucket argument value.
|
||||
DstBucket string
|
||||
// UploadID is the uploadID argument value.
|
||||
UploadID string
|
||||
// RangeHeader is the rangeHeader argument value.
|
||||
RangeHeader string
|
||||
// Part is the part argument value.
|
||||
Part int
|
||||
}
|
||||
// CreateMultipartUpload holds details about calls to the CreateMultipartUpload method.
|
||||
CreateMultipartUpload []struct {
|
||||
// CreateMultipartUploadInput is the createMultipartUploadInput argument value.
|
||||
@@ -339,8 +318,8 @@ type BackendMock struct {
|
||||
}
|
||||
// ListMultipartUploads holds details about calls to the ListMultipartUploads method.
|
||||
ListMultipartUploads []struct {
|
||||
// Output is the output argument value.
|
||||
Output *s3.ListMultipartUploadsInput
|
||||
// ListMultipartUploadsInput is the listMultipartUploadsInput argument value.
|
||||
ListMultipartUploadsInput *s3.ListMultipartUploadsInput
|
||||
}
|
||||
// ListObjectParts holds details about calls to the ListObjectParts method.
|
||||
ListObjectParts []struct {
|
||||
@@ -460,7 +439,6 @@ type BackendMock struct {
|
||||
lockAbortMultipartUpload sync.RWMutex
|
||||
lockCompleteMultipartUpload sync.RWMutex
|
||||
lockCopyObject sync.RWMutex
|
||||
lockCopyPart sync.RWMutex
|
||||
lockCreateMultipartUpload sync.RWMutex
|
||||
lockDeleteBucket sync.RWMutex
|
||||
lockDeleteObject sync.RWMutex
|
||||
@@ -567,7 +545,7 @@ func (mock *BackendMock) CompleteMultipartUploadCalls() []struct {
|
||||
}
|
||||
|
||||
// CopyObject calls CopyObjectFunc.
|
||||
func (mock *BackendMock) CopyObject(srcBucket string, srcObject string, DstBucket string, dstObject string) (*s3.CopyObjectOutput, error) {
|
||||
func (mock *BackendMock) CopyObject(srcBucket string, srcObject string, dstBucket string, dstObject string) (*s3.CopyObjectOutput, error) {
|
||||
if mock.CopyObjectFunc == nil {
|
||||
panic("BackendMock.CopyObjectFunc: method is nil but Backend.CopyObject was just called")
|
||||
}
|
||||
@@ -579,13 +557,13 @@ func (mock *BackendMock) CopyObject(srcBucket string, srcObject string, DstBucke
|
||||
}{
|
||||
SrcBucket: srcBucket,
|
||||
SrcObject: srcObject,
|
||||
DstBucket: DstBucket,
|
||||
DstBucket: dstBucket,
|
||||
DstObject: dstObject,
|
||||
}
|
||||
mock.lockCopyObject.Lock()
|
||||
mock.calls.CopyObject = append(mock.calls.CopyObject, callInfo)
|
||||
mock.lockCopyObject.Unlock()
|
||||
return mock.CopyObjectFunc(srcBucket, srcObject, DstBucket, dstObject)
|
||||
return mock.CopyObjectFunc(srcBucket, srcObject, dstBucket, dstObject)
|
||||
}
|
||||
|
||||
// CopyObjectCalls gets all the calls that were made to CopyObject.
|
||||
@@ -610,58 +588,6 @@ func (mock *BackendMock) CopyObjectCalls() []struct {
|
||||
return calls
|
||||
}
|
||||
|
||||
// CopyPart calls CopyPartFunc.
|
||||
func (mock *BackendMock) CopyPart(srcBucket string, srcObject string, DstBucket string, uploadID string, rangeHeader string, part int) (*types.CopyPartResult, error) {
|
||||
if mock.CopyPartFunc == nil {
|
||||
panic("BackendMock.CopyPartFunc: method is nil but Backend.CopyPart was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
SrcBucket string
|
||||
SrcObject string
|
||||
DstBucket string
|
||||
UploadID string
|
||||
RangeHeader string
|
||||
Part int
|
||||
}{
|
||||
SrcBucket: srcBucket,
|
||||
SrcObject: srcObject,
|
||||
DstBucket: DstBucket,
|
||||
UploadID: uploadID,
|
||||
RangeHeader: rangeHeader,
|
||||
Part: part,
|
||||
}
|
||||
mock.lockCopyPart.Lock()
|
||||
mock.calls.CopyPart = append(mock.calls.CopyPart, callInfo)
|
||||
mock.lockCopyPart.Unlock()
|
||||
return mock.CopyPartFunc(srcBucket, srcObject, DstBucket, uploadID, rangeHeader, part)
|
||||
}
|
||||
|
||||
// CopyPartCalls gets all the calls that were made to CopyPart.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedBackend.CopyPartCalls())
|
||||
func (mock *BackendMock) CopyPartCalls() []struct {
|
||||
SrcBucket string
|
||||
SrcObject string
|
||||
DstBucket string
|
||||
UploadID string
|
||||
RangeHeader string
|
||||
Part int
|
||||
} {
|
||||
var calls []struct {
|
||||
SrcBucket string
|
||||
SrcObject string
|
||||
DstBucket string
|
||||
UploadID string
|
||||
RangeHeader string
|
||||
Part int
|
||||
}
|
||||
mock.lockCopyPart.RLock()
|
||||
calls = mock.calls.CopyPart
|
||||
mock.lockCopyPart.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// CreateMultipartUpload calls CreateMultipartUploadFunc.
|
||||
func (mock *BackendMock) CreateMultipartUpload(createMultipartUploadInput *s3.CreateMultipartUploadInput) (*s3.CreateMultipartUploadOutput, error) {
|
||||
if mock.CreateMultipartUploadFunc == nil {
|
||||
@@ -1082,19 +1008,19 @@ func (mock *BackendMock) ListBucketsCalls() []struct {
|
||||
}
|
||||
|
||||
// ListMultipartUploads calls ListMultipartUploadsFunc.
|
||||
func (mock *BackendMock) ListMultipartUploads(output *s3.ListMultipartUploadsInput) (s3response.ListMultipartUploadsResponse, error) {
|
||||
func (mock *BackendMock) ListMultipartUploads(listMultipartUploadsInput *s3.ListMultipartUploadsInput) (s3response.ListMultipartUploadsResponse, error) {
|
||||
if mock.ListMultipartUploadsFunc == nil {
|
||||
panic("BackendMock.ListMultipartUploadsFunc: method is nil but Backend.ListMultipartUploads was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
Output *s3.ListMultipartUploadsInput
|
||||
ListMultipartUploadsInput *s3.ListMultipartUploadsInput
|
||||
}{
|
||||
Output: output,
|
||||
ListMultipartUploadsInput: listMultipartUploadsInput,
|
||||
}
|
||||
mock.lockListMultipartUploads.Lock()
|
||||
mock.calls.ListMultipartUploads = append(mock.calls.ListMultipartUploads, callInfo)
|
||||
mock.lockListMultipartUploads.Unlock()
|
||||
return mock.ListMultipartUploadsFunc(output)
|
||||
return mock.ListMultipartUploadsFunc(listMultipartUploadsInput)
|
||||
}
|
||||
|
||||
// ListMultipartUploadsCalls gets all the calls that were made to ListMultipartUploads.
|
||||
@@ -1102,10 +1028,10 @@ func (mock *BackendMock) ListMultipartUploads(output *s3.ListMultipartUploadsInp
|
||||
//
|
||||
// len(mockedBackend.ListMultipartUploadsCalls())
|
||||
func (mock *BackendMock) ListMultipartUploadsCalls() []struct {
|
||||
Output *s3.ListMultipartUploadsInput
|
||||
ListMultipartUploadsInput *s3.ListMultipartUploadsInput
|
||||
} {
|
||||
var calls []struct {
|
||||
Output *s3.ListMultipartUploadsInput
|
||||
ListMultipartUploadsInput *s3.ListMultipartUploadsInput
|
||||
}
|
||||
mock.lockListMultipartUploads.RLock()
|
||||
calls = mock.calls.ListMultipartUploads
|
||||
@@ -1616,7 +1542,7 @@ func (mock *BackendMock) StringCalls() []struct {
|
||||
}
|
||||
|
||||
// UploadPartCopy calls UploadPartCopyFunc.
|
||||
func (mock *BackendMock) UploadPartCopy(uploadPartCopyInput *s3.UploadPartCopyInput) (*s3.UploadPartCopyOutput, error) {
|
||||
func (mock *BackendMock) UploadPartCopy(uploadPartCopyInput *s3.UploadPartCopyInput) (s3response.CopyObjectResult, error) {
|
||||
if mock.UploadPartCopyFunc == nil {
|
||||
panic("BackendMock.UploadPartCopyFunc: method is nil but Backend.UploadPartCopy was just called")
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ package controllers
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
@@ -33,24 +32,27 @@ import (
|
||||
"github.com/versity/versitygw/backend"
|
||||
"github.com/versity/versitygw/s3api/utils"
|
||||
"github.com/versity/versitygw/s3err"
|
||||
"github.com/versity/versitygw/s3log"
|
||||
"github.com/versity/versitygw/s3response"
|
||||
)
|
||||
|
||||
type S3ApiController struct {
|
||||
be backend.Backend
|
||||
iam auth.IAMService
|
||||
be backend.Backend
|
||||
iam auth.IAMService
|
||||
logger s3log.Logger
|
||||
}
|
||||
|
||||
func New(be backend.Backend) S3ApiController {
|
||||
return S3ApiController{be: be}
|
||||
func New(be backend.Backend, iam auth.IAMService, logger s3log.Logger) S3ApiController {
|
||||
return S3ApiController{be: be, iam: iam, logger: logger}
|
||||
}
|
||||
|
||||
func (c S3ApiController) ListBuckets(ctx *fiber.Ctx) error {
|
||||
access, isRoot := ctx.Locals("access").(string), ctx.Locals("isRoot").(bool)
|
||||
if err := auth.IsAdmin(access, isRoot); err != nil {
|
||||
return SendXMLResponse(ctx, nil, err)
|
||||
return SendXMLResponse(ctx, nil, err, LogOptions{Logger: c.logger, Action: "ListBucket"})
|
||||
}
|
||||
res, err := c.be.ListBuckets()
|
||||
return SendXMLResponse(ctx, res, err)
|
||||
return SendXMLResponse(ctx, res, err, LogOptions{Logger: c.logger, Action: "ListBucket"})
|
||||
}
|
||||
|
||||
func (c S3ApiController) GetActions(ctx *fiber.Ctx) error {
|
||||
@@ -69,57 +71,85 @@ func (c S3ApiController) GetActions(ctx *fiber.Ctx) error {
|
||||
|
||||
data, err := c.be.GetBucketAcl(bucket)
|
||||
if err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
return SendResponse(ctx, err, LogOptions{Logger: c.logger, Bucket: &bucket, Object: &key})
|
||||
}
|
||||
|
||||
parsedAcl, err := auth.ParseACL(data)
|
||||
if err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
return SendResponse(ctx, err, LogOptions{Logger: c.logger})
|
||||
}
|
||||
|
||||
if ctx.Request().URI().QueryArgs().Has("tagging") {
|
||||
if err := auth.VerifyACL(parsedAcl, bucket, access, "READ", isRoot); err != nil {
|
||||
return SendXMLResponse(ctx, nil, err, LogOptions{Logger: c.logger, Action: "GetObjectTagging", Bucket: &bucket, Object: &key})
|
||||
}
|
||||
|
||||
tags, err := c.be.GetTags(bucket, key)
|
||||
if err != nil {
|
||||
return SendXMLResponse(ctx, nil, err, LogOptions{Logger: c.logger, Action: "GetObjectTagging", Bucket: &bucket, Object: &key})
|
||||
}
|
||||
resp := s3response.Tagging{TagSet: s3response.TagSet{Tags: []s3response.Tag{}}}
|
||||
|
||||
for key, val := range tags {
|
||||
resp.TagSet.Tags = append(resp.TagSet.Tags, s3response.Tag{Key: key, Value: val})
|
||||
}
|
||||
|
||||
return SendXMLResponse(ctx, resp, nil, LogOptions{Logger: c.logger, Action: "GetObjectTagging", Bucket: &bucket, Object: &key})
|
||||
}
|
||||
|
||||
if uploadId != "" {
|
||||
if maxParts < 0 || (maxParts == 0 && ctx.Query("max-parts") != "") {
|
||||
return SendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidMaxParts))
|
||||
return SendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidMaxParts), LogOptions{
|
||||
Logger: c.logger,
|
||||
Action: "ListObjectParts",
|
||||
Bucket: &bucket,
|
||||
Object: &key,
|
||||
})
|
||||
}
|
||||
if partNumberMarker < 0 || (partNumberMarker == 0 && ctx.Query("part-number-marker") != "") {
|
||||
return SendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidPartNumberMarker))
|
||||
return SendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidPartNumberMarker), LogOptions{
|
||||
Logger: c.logger,
|
||||
Action: "ListObjectParts",
|
||||
Bucket: &bucket,
|
||||
Object: &key,
|
||||
})
|
||||
}
|
||||
|
||||
if err := auth.VerifyACL(parsedAcl, bucket, access, "READ", isRoot); err != nil {
|
||||
return SendXMLResponse(ctx, nil, err)
|
||||
return SendXMLResponse(ctx, nil, err, LogOptions{Logger: c.logger, Action: "ListObjectParts", Bucket: &bucket, Object: &key})
|
||||
}
|
||||
|
||||
res, err := c.be.ListObjectParts(bucket, key, uploadId, partNumberMarker, maxParts)
|
||||
return SendXMLResponse(ctx, res, err)
|
||||
return SendXMLResponse(ctx, res, err, LogOptions{Logger: c.logger, Action: "ListObjectParts", Bucket: &bucket, Object: &key})
|
||||
}
|
||||
|
||||
if ctx.Request().URI().QueryArgs().Has("acl") {
|
||||
if err := auth.VerifyACL(parsedAcl, bucket, access, "READ_ACP", isRoot); err != nil {
|
||||
return SendXMLResponse(ctx, nil, err)
|
||||
return SendXMLResponse(ctx, nil, err, LogOptions{Logger: c.logger, Action: "GetObjectAcl", Bucket: &bucket, Object: &key})
|
||||
}
|
||||
res, err := c.be.GetObjectAcl(bucket, key)
|
||||
return SendXMLResponse(ctx, res, err)
|
||||
return SendXMLResponse(ctx, res, err, LogOptions{Logger: c.logger, Action: "GetObjectAcl", Bucket: &bucket, Object: &key})
|
||||
}
|
||||
|
||||
if attrs := ctx.Get("X-Amz-Object-Attributes"); attrs != "" {
|
||||
if err := auth.VerifyACL(parsedAcl, bucket, access, "READ", isRoot); err != nil {
|
||||
return SendXMLResponse(ctx, nil, err)
|
||||
return SendXMLResponse(ctx, nil, err, LogOptions{Logger: c.logger, Action: "GetObjectAttributes", Bucket: &bucket, Object: &key})
|
||||
}
|
||||
res, err := c.be.GetObjectAttributes(bucket, key, strings.Split(attrs, ","))
|
||||
return SendXMLResponse(ctx, res, err)
|
||||
return SendXMLResponse(ctx, res, err, LogOptions{Logger: c.logger, Action: "GetObjectAttributes", Bucket: &bucket, Object: &key})
|
||||
}
|
||||
|
||||
if err := auth.VerifyACL(parsedAcl, bucket, access, "READ_ACP", isRoot); err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
return SendResponse(ctx, err, LogOptions{Logger: c.logger, Bucket: &bucket, Object: &key, Action: "GetObject"})
|
||||
}
|
||||
|
||||
ctx.Locals("logResBody", false)
|
||||
res, err := c.be.GetObject(bucket, key, acceptRange, ctx.Response().BodyWriter())
|
||||
if err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
return SendResponse(ctx, err, LogOptions{Logger: c.logger, Bucket: &bucket, Object: &key, Action: "GetObject"})
|
||||
}
|
||||
if res == nil {
|
||||
return SendResponse(ctx, fmt.Errorf("get object nil response"))
|
||||
return SendResponse(ctx, fmt.Errorf("get object nil response"), LogOptions{Logger: c.logger, Bucket: &bucket, Object: &key, Action: "GetObject"})
|
||||
}
|
||||
|
||||
utils.SetMetaHeaders(ctx, res.Metadata)
|
||||
@@ -153,7 +183,7 @@ func (c S3ApiController) GetActions(ctx *fiber.Ctx) error {
|
||||
Value: string(res.StorageClass),
|
||||
},
|
||||
})
|
||||
return SendResponse(ctx, err)
|
||||
return SendResponse(ctx, err, LogOptions{Logger: c.logger, Bucket: &bucket, Object: &key, Action: "GetObject"})
|
||||
}
|
||||
|
||||
func getstring(s *string) string {
|
||||
@@ -174,50 +204,51 @@ func (c S3ApiController) ListActions(ctx *fiber.Ctx) error {
|
||||
|
||||
data, err := c.be.GetBucketAcl(bucket)
|
||||
if err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
return SendResponse(ctx, err, LogOptions{Logger: c.logger, Bucket: &bucket})
|
||||
}
|
||||
|
||||
parsedAcl, err := auth.ParseACL(data)
|
||||
if err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
return SendResponse(ctx, err, LogOptions{Logger: c.logger})
|
||||
}
|
||||
|
||||
if ctx.Request().URI().QueryArgs().Has("acl") {
|
||||
if err := auth.VerifyACL(parsedAcl, bucket, access, "READ_ACP", isRoot); err != nil {
|
||||
return SendXMLResponse(ctx, nil, err)
|
||||
return SendXMLResponse(ctx, nil, err, LogOptions{Logger: c.logger, Action: "GetBucketAcl", Bucket: &bucket})
|
||||
}
|
||||
|
||||
res, err := auth.ParseACLOutput(data)
|
||||
return SendXMLResponse(ctx, res, err)
|
||||
return SendXMLResponse(ctx, res, err, LogOptions{Logger: c.logger, Action: "GetBucketAcl", Bucket: &bucket})
|
||||
}
|
||||
|
||||
if ctx.Request().URI().QueryArgs().Has("uploads") {
|
||||
if err := auth.VerifyACL(parsedAcl, bucket, access, "READ", isRoot); err != nil {
|
||||
return SendXMLResponse(ctx, nil, err)
|
||||
return SendXMLResponse(ctx, nil, err, LogOptions{Logger: c.logger, Action: "ListMultipartUploads", Bucket: &bucket})
|
||||
}
|
||||
res, err := c.be.ListMultipartUploads(&s3.ListMultipartUploadsInput{Bucket: aws.String(ctx.Params("bucket"))})
|
||||
return SendXMLResponse(ctx, res, err)
|
||||
return SendXMLResponse(ctx, res, err, LogOptions{Logger: c.logger, Action: "ListMultipartUploads", Bucket: &bucket})
|
||||
}
|
||||
|
||||
if ctx.QueryInt("list-type") == 2 {
|
||||
if err := auth.VerifyACL(parsedAcl, bucket, access, "READ", isRoot); err != nil {
|
||||
return SendXMLResponse(ctx, nil, err)
|
||||
return SendXMLResponse(ctx, nil, err, LogOptions{Logger: c.logger, Action: "ListObjectsV2", Bucket: &bucket})
|
||||
}
|
||||
res, err := c.be.ListObjectsV2(bucket, prefix, marker, delimiter, maxkeys)
|
||||
return SendXMLResponse(ctx, res, err)
|
||||
return SendXMLResponse(ctx, res, err, LogOptions{Logger: c.logger, Action: "ListObjectsV2", Bucket: &bucket})
|
||||
}
|
||||
|
||||
if err := auth.VerifyACL(parsedAcl, bucket, access, "READ", isRoot); err != nil {
|
||||
return SendXMLResponse(ctx, nil, err)
|
||||
return SendXMLResponse(ctx, nil, err, LogOptions{Logger: c.logger, Action: "ListObjects", Bucket: &bucket})
|
||||
}
|
||||
|
||||
res, err := c.be.ListObjects(bucket, prefix, marker, delimiter, maxkeys)
|
||||
return SendXMLResponse(ctx, res, err)
|
||||
return SendXMLResponse(ctx, res, err, LogOptions{Logger: c.logger, Action: "ListObjects", Bucket: &bucket})
|
||||
}
|
||||
|
||||
func (c S3ApiController) PutBucketActions(ctx *fiber.Ctx) error {
|
||||
bucket, acl, grantFullControl, grantRead, grantReadACP, granWrite, grantWriteACP, access, isRoot :=
|
||||
bucket, bucketOwner, acl, grantFullControl, grantRead, grantReadACP, granWrite, grantWriteACP, access, isRoot :=
|
||||
ctx.Params("bucket"),
|
||||
ctx.Get("X-Amz-Expected-Bucket-Owner"),
|
||||
ctx.Get("X-Amz-Acl"),
|
||||
ctx.Get("X-Amz-Grant-Full-Control"),
|
||||
ctx.Get("X-Amz-Grant-Read"),
|
||||
@@ -229,46 +260,78 @@ func (c S3ApiController) PutBucketActions(ctx *fiber.Ctx) error {
|
||||
|
||||
grants := grantFullControl + grantRead + grantReadACP + granWrite + grantWriteACP
|
||||
|
||||
if grants != "" || acl != "" {
|
||||
if grants != "" && acl != "" {
|
||||
return errors.New("wrong api call")
|
||||
}
|
||||
if ctx.Request().URI().QueryArgs().Has("acl") {
|
||||
var input *s3.PutBucketAclInput
|
||||
|
||||
if acl != "" && acl != "private" && acl != "public-read" && acl != "public-read-write" {
|
||||
return errors.New("wrong api call")
|
||||
if len(ctx.Body()) > 0 {
|
||||
if grants+acl != "" {
|
||||
return SendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidRequest), LogOptions{Logger: c.logger, Action: "PutBucketAcl", Bucket: &bucket})
|
||||
}
|
||||
|
||||
var accessControlPolicy auth.AccessControlPolicy
|
||||
err := xml.Unmarshal(ctx.Body(), &accessControlPolicy)
|
||||
if err != nil {
|
||||
return SendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidRequest), LogOptions{Logger: c.logger, Action: "PutBucketAcl", Bucket: &bucket})
|
||||
}
|
||||
|
||||
input = &s3.PutBucketAclInput{
|
||||
Bucket: &bucket,
|
||||
ACL: "",
|
||||
AccessControlPolicy: &types.AccessControlPolicy{Owner: &accessControlPolicy.Owner, Grants: accessControlPolicy.AccessControlList.Grants},
|
||||
}
|
||||
}
|
||||
if acl != "" {
|
||||
if acl != "private" && acl != "public-read" && acl != "public-read-write" {
|
||||
return SendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidRequest), LogOptions{Logger: c.logger, Action: "PutBucketAcl", Bucket: &bucket})
|
||||
}
|
||||
if len(ctx.Body()) > 0 || grants != "" {
|
||||
return SendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidRequest), LogOptions{Logger: c.logger, Action: "PutBucketAcl", Bucket: &bucket})
|
||||
}
|
||||
|
||||
input = &s3.PutBucketAclInput{
|
||||
Bucket: &bucket,
|
||||
ACL: types.BucketCannedACL(acl),
|
||||
AccessControlPolicy: &types.AccessControlPolicy{Owner: &types.Owner{ID: &bucketOwner}},
|
||||
}
|
||||
}
|
||||
if grants != "" {
|
||||
input = &s3.PutBucketAclInput{
|
||||
Bucket: &bucket,
|
||||
GrantFullControl: &grantFullControl,
|
||||
GrantRead: &grantRead,
|
||||
GrantReadACP: &grantReadACP,
|
||||
GrantWrite: &granWrite,
|
||||
GrantWriteACP: &grantWriteACP,
|
||||
AccessControlPolicy: &types.AccessControlPolicy{Owner: &types.Owner{ID: &bucketOwner}},
|
||||
ACL: "",
|
||||
}
|
||||
}
|
||||
|
||||
data, err := c.be.GetBucketAcl(bucket)
|
||||
if err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
return SendResponse(ctx, err, LogOptions{Logger: c.logger, Action: "PutBucketAcl", Bucket: &bucket})
|
||||
}
|
||||
|
||||
parsedAcl, err := auth.ParseACL(data)
|
||||
if err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
return SendResponse(ctx, err, LogOptions{Logger: c.logger, Action: "PutBucketAcl", Bucket: &bucket})
|
||||
}
|
||||
|
||||
if err := auth.VerifyACL(parsedAcl, bucket, access, "WRITE_ACP", isRoot); err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
return SendResponse(ctx, err, LogOptions{Logger: c.logger, Action: "PutBucketAcl", Bucket: &bucket})
|
||||
}
|
||||
|
||||
input := &s3.PutBucketAclInput{
|
||||
Bucket: &bucket,
|
||||
ACL: types.BucketCannedACL(acl),
|
||||
GrantFullControl: &grantFullControl,
|
||||
GrantRead: &grantRead,
|
||||
GrantReadACP: &grantReadACP,
|
||||
GrantWrite: &granWrite,
|
||||
GrantWriteACP: &grantWriteACP,
|
||||
AccessControlPolicy: &types.AccessControlPolicy{Owner: &types.Owner{ID: &access}},
|
||||
updAcl, err := auth.UpdateACL(input, parsedAcl, c.iam)
|
||||
if err != nil {
|
||||
return SendResponse(ctx, err, LogOptions{Logger: c.logger, Action: "PutBucketAcl", Bucket: &bucket})
|
||||
}
|
||||
|
||||
err = auth.UpdateACL(input, parsedAcl, c.iam)
|
||||
return SendResponse(ctx, err)
|
||||
err = c.be.PutBucketAcl(bucket, updAcl)
|
||||
return SendResponse(ctx, err, LogOptions{Logger: c.logger, Action: "PutBucketAcl", Bucket: &bucket})
|
||||
}
|
||||
|
||||
err := c.be.PutBucket(bucket, access)
|
||||
return SendResponse(ctx, err)
|
||||
return SendResponse(ctx, err, LogOptions{Logger: c.logger, Action: "PutBucket", Bucket: &bucket})
|
||||
}
|
||||
|
||||
func (c S3ApiController) PutActions(ctx *fiber.Ctx) error {
|
||||
@@ -276,7 +339,6 @@ func (c S3ApiController) PutActions(ctx *fiber.Ctx) error {
|
||||
keyStart := ctx.Params("key")
|
||||
keyEnd := ctx.Params("*1")
|
||||
uploadId := ctx.Query("uploadId")
|
||||
partNumberStr := ctx.Query("partNumber")
|
||||
access := ctx.Locals("access").(string)
|
||||
isRoot := ctx.Locals("isRoot").(bool)
|
||||
|
||||
@@ -286,6 +348,7 @@ func (c S3ApiController) PutActions(ctx *fiber.Ctx) error {
|
||||
copySrcIfNoneMatch := ctx.Get("X-Amz-Copy-Source-If-None-Match")
|
||||
copySrcModifSince := ctx.Get("X-Amz-Copy-Source-If-Modified-Since")
|
||||
copySrcUnmodifSince := ctx.Get("X-Amz-Copy-Source-If-Unmodified-Since")
|
||||
copySrcRange := ctx.Get("X-Amz-Copy-Source-Range")
|
||||
|
||||
// Permission headers
|
||||
acl := ctx.Get("X-Amz-Acl")
|
||||
@@ -297,6 +360,7 @@ func (c S3ApiController) PutActions(ctx *fiber.Ctx) error {
|
||||
|
||||
// Other headers
|
||||
contentLengthStr := ctx.Get("Content-Length")
|
||||
bucketOwner := ctx.Get("X-Amz-Expected-Bucket-Owner")
|
||||
|
||||
grants := grantFullControl + grantRead + grantReadACP + granWrite + grantWriteACP
|
||||
|
||||
@@ -308,33 +372,69 @@ func (c S3ApiController) PutActions(ctx *fiber.Ctx) error {
|
||||
keyStart = keyStart + "/"
|
||||
}
|
||||
|
||||
var contentLength int64
|
||||
if contentLengthStr != "" {
|
||||
var err error
|
||||
contentLength, err = strconv.ParseInt(contentLengthStr, 10, 64)
|
||||
if err != nil {
|
||||
return SendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidRequest))
|
||||
}
|
||||
}
|
||||
|
||||
data, err := c.be.GetBucketAcl(bucket)
|
||||
if err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
return SendResponse(ctx, err, LogOptions{Logger: c.logger, Bucket: &bucket, Object: &keyStart})
|
||||
}
|
||||
|
||||
parsedAcl, err := auth.ParseACL(data)
|
||||
if err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
return SendResponse(ctx, err, LogOptions{Logger: c.logger})
|
||||
}
|
||||
|
||||
if uploadId != "" && partNumberStr != "" {
|
||||
partNumber := ctx.QueryInt("partNumber", -1)
|
||||
if partNumber < 1 {
|
||||
return SendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidPart))
|
||||
if ctx.Request().URI().QueryArgs().Has("tagging") {
|
||||
var objTagging s3response.Tagging
|
||||
err := xml.Unmarshal(ctx.Body(), &objTagging)
|
||||
if err != nil {
|
||||
return SendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidRequest), LogOptions{Logger: c.logger, Action: "PutObjectTagging", Bucket: &bucket, Object: &keyStart})
|
||||
}
|
||||
|
||||
tags := make(map[string]string, len(objTagging.TagSet.Tags))
|
||||
|
||||
for _, tag := range objTagging.TagSet.Tags {
|
||||
tags[tag.Key] = tag.Value
|
||||
}
|
||||
|
||||
if err := auth.VerifyACL(parsedAcl, bucket, access, "WRITE", isRoot); err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
return SendResponse(ctx, err, LogOptions{Logger: c.logger, Action: "PutObjectTagging", Bucket: &bucket, Object: &keyStart})
|
||||
}
|
||||
|
||||
err = c.be.SetTags(bucket, keyStart, tags)
|
||||
|
||||
return SendResponse(ctx, err, LogOptions{Logger: c.logger, Action: "PutObjectTagging", Bucket: &bucket, Object: &keyStart})
|
||||
}
|
||||
|
||||
if ctx.Request().URI().QueryArgs().Has("uploadId") && ctx.Request().URI().QueryArgs().Has("partNumber") && copySource != "" {
|
||||
partNumber := ctx.QueryInt("partNumber", -1)
|
||||
if partNumber < 1 || partNumber > 10000 {
|
||||
return SendXMLResponse(ctx, nil, s3err.GetAPIError(s3err.ErrInvalidPart), LogOptions{Logger: c.logger, Action: "UploadPartCopy", Bucket: &bucket, Object: &keyStart})
|
||||
}
|
||||
|
||||
resp, err := c.be.UploadPartCopy(&s3.UploadPartCopyInput{
|
||||
Bucket: &bucket,
|
||||
Key: &keyStart,
|
||||
CopySource: ©Source,
|
||||
PartNumber: int32(partNumber),
|
||||
UploadId: &uploadId,
|
||||
ExpectedBucketOwner: &bucketOwner,
|
||||
CopySourceRange: ©SrcRange,
|
||||
})
|
||||
return SendXMLResponse(ctx, resp, err, LogOptions{Logger: c.logger, Action: "UploadPartCopy", Bucket: &bucket, Object: &keyStart})
|
||||
}
|
||||
|
||||
if ctx.Request().URI().QueryArgs().Has("uploadId") && ctx.Request().URI().QueryArgs().Has("partNumber") {
|
||||
partNumber := ctx.QueryInt("partNumber", -1)
|
||||
if partNumber < 1 || partNumber > 10000 {
|
||||
return SendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidPart), LogOptions{Logger: c.logger, Action: "PutObjectPart", Bucket: &bucket, Object: &keyStart})
|
||||
}
|
||||
|
||||
if err := auth.VerifyACL(parsedAcl, bucket, access, "WRITE", isRoot); err != nil {
|
||||
return SendResponse(ctx, err, LogOptions{Logger: c.logger, Action: "PutObjectPart", Bucket: &bucket, Object: &keyStart})
|
||||
}
|
||||
|
||||
contentLength, err := strconv.ParseInt(contentLengthStr, 10, 64)
|
||||
if err != nil {
|
||||
return SendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidRequest), LogOptions{Logger: c.logger, Action: "PutObjectPart", Bucket: &bucket, Object: &keyStart})
|
||||
}
|
||||
|
||||
body := io.ReadSeeker(bytes.NewReader([]byte(ctx.Body())))
|
||||
@@ -342,29 +442,61 @@ func (c S3ApiController) PutActions(ctx *fiber.Ctx) error {
|
||||
etag, err := c.be.PutObjectPart(bucket, keyStart, uploadId,
|
||||
partNumber, contentLength, body)
|
||||
ctx.Response().Header.Set("Etag", etag)
|
||||
return SendResponse(ctx, err)
|
||||
return SendResponse(ctx, err, LogOptions{Logger: c.logger, Action: "PutObjectPart", Bucket: &bucket, Object: &keyStart})
|
||||
}
|
||||
|
||||
if grants != "" || acl != "" {
|
||||
if grants != "" && acl != "" {
|
||||
return errors.New("wrong api call")
|
||||
if ctx.Request().URI().QueryArgs().Has("acl") {
|
||||
var input *s3.PutObjectAclInput
|
||||
|
||||
if len(ctx.Body()) > 0 {
|
||||
if grants+acl != "" {
|
||||
return SendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidRequest), LogOptions{Logger: c.logger, Action: "PutObjectAcl", Bucket: &bucket, Object: &keyStart})
|
||||
}
|
||||
|
||||
var accessControlPolicy auth.AccessControlPolicy
|
||||
err := xml.Unmarshal(ctx.Body(), &accessControlPolicy)
|
||||
if err != nil {
|
||||
return SendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidRequest), LogOptions{Logger: c.logger, Action: "PutObjectAcl", Bucket: &bucket, Object: &keyStart})
|
||||
}
|
||||
|
||||
input = &s3.PutObjectAclInput{
|
||||
Bucket: &bucket,
|
||||
Key: &keyStart,
|
||||
ACL: "",
|
||||
AccessControlPolicy: &types.AccessControlPolicy{Owner: &accessControlPolicy.Owner, Grants: accessControlPolicy.AccessControlList.Grants},
|
||||
}
|
||||
}
|
||||
if acl != "" {
|
||||
if acl != "private" && acl != "public-read" && acl != "public-read-write" {
|
||||
return SendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidRequest), LogOptions{Logger: c.logger, Action: "PutObjectAcl", Bucket: &bucket, Object: &keyStart})
|
||||
}
|
||||
if len(ctx.Body()) > 0 || grants != "" {
|
||||
return SendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidRequest), LogOptions{Logger: c.logger, Action: "PutObjectAcl", Bucket: &bucket, Object: &keyStart})
|
||||
}
|
||||
|
||||
input = &s3.PutObjectAclInput{
|
||||
Bucket: &bucket,
|
||||
Key: &keyStart,
|
||||
ACL: types.ObjectCannedACL(acl),
|
||||
AccessControlPolicy: &types.AccessControlPolicy{Owner: &types.Owner{ID: &bucketOwner}},
|
||||
}
|
||||
}
|
||||
if grants != "" {
|
||||
input = &s3.PutObjectAclInput{
|
||||
Bucket: &bucket,
|
||||
Key: &keyStart,
|
||||
GrantFullControl: &grantFullControl,
|
||||
GrantRead: &grantRead,
|
||||
GrantReadACP: &grantReadACP,
|
||||
GrantWrite: &granWrite,
|
||||
GrantWriteACP: &grantWriteACP,
|
||||
AccessControlPolicy: &types.AccessControlPolicy{Owner: &types.Owner{ID: &bucketOwner}},
|
||||
ACL: "",
|
||||
}
|
||||
}
|
||||
|
||||
if err := auth.VerifyACL(parsedAcl, bucket, access, "WRITE_ACP", isRoot); err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
}
|
||||
|
||||
err := c.be.PutObjectAcl(&s3.PutObjectAclInput{
|
||||
Bucket: &bucket,
|
||||
Key: &keyStart,
|
||||
ACL: types.ObjectCannedACL(acl),
|
||||
GrantFullControl: &grantFullControl,
|
||||
GrantRead: &grantRead,
|
||||
GrantReadACP: &grantReadACP,
|
||||
GrantWrite: &granWrite,
|
||||
GrantWriteACP: &grantWriteACP,
|
||||
})
|
||||
return SendResponse(ctx, err)
|
||||
err = c.be.PutObjectAcl(input)
|
||||
return SendResponse(ctx, err, LogOptions{Logger: c.logger, Action: "PutObjectAcl", Bucket: &bucket, Object: &keyStart})
|
||||
}
|
||||
|
||||
if copySource != "" {
|
||||
@@ -374,17 +506,22 @@ func (c S3ApiController) PutActions(ctx *fiber.Ctx) error {
|
||||
srcBucket, srcObject := copySourceSplit[0], copySourceSplit[1:]
|
||||
|
||||
if err := auth.VerifyACL(parsedAcl, bucket, access, "WRITE", isRoot); err != nil {
|
||||
return SendXMLResponse(ctx, nil, err)
|
||||
return SendXMLResponse(ctx, nil, err, LogOptions{Logger: c.logger, Action: "CopyObject", Bucket: &bucket, Object: &keyStart})
|
||||
}
|
||||
|
||||
res, err := c.be.CopyObject(srcBucket, strings.Join(srcObject, "/"), bucket, keyStart)
|
||||
return SendXMLResponse(ctx, res, err)
|
||||
return SendXMLResponse(ctx, res, err, LogOptions{Logger: c.logger, Action: "CopyObject", Bucket: &bucket, Object: &keyStart})
|
||||
}
|
||||
|
||||
metadata := utils.GetUserMetaData(&ctx.Request().Header)
|
||||
|
||||
if err := auth.VerifyACL(parsedAcl, bucket, access, "WRITE", isRoot); err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
return SendResponse(ctx, err, LogOptions{Logger: c.logger, Action: "PutObject", Bucket: &bucket, Object: &keyStart})
|
||||
}
|
||||
|
||||
contentLength, err := strconv.ParseInt(contentLengthStr, 10, 64)
|
||||
if err != nil {
|
||||
return SendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidRequest), LogOptions{Logger: c.logger, Action: "PutObject", Bucket: &bucket, Object: &keyStart})
|
||||
}
|
||||
|
||||
ctx.Locals("logReqBody", false)
|
||||
@@ -396,7 +533,7 @@ func (c S3ApiController) PutActions(ctx *fiber.Ctx) error {
|
||||
Body: bytes.NewReader(ctx.Request().Body()),
|
||||
})
|
||||
ctx.Response().Header.Set("ETag", etag)
|
||||
return SendResponse(ctx, err)
|
||||
return SendResponse(ctx, err, LogOptions{Logger: c.logger, Action: "PutObject", Bucket: &bucket, Object: &keyStart})
|
||||
}
|
||||
|
||||
func (c S3ApiController) DeleteBucket(ctx *fiber.Ctx) error {
|
||||
@@ -404,20 +541,20 @@ func (c S3ApiController) DeleteBucket(ctx *fiber.Ctx) error {
|
||||
|
||||
data, err := c.be.GetBucketAcl(bucket)
|
||||
if err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
return SendResponse(ctx, err, LogOptions{Logger: c.logger, Action: "DeleteBucket", Bucket: &bucket})
|
||||
}
|
||||
|
||||
parsedAcl, err := auth.ParseACL(data)
|
||||
if err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
return SendResponse(ctx, err, LogOptions{Logger: c.logger, Action: "DeleteBucket", Bucket: &bucket})
|
||||
}
|
||||
|
||||
if err := auth.VerifyACL(parsedAcl, bucket, access, "WRITE", isRoot); err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
return SendResponse(ctx, err, LogOptions{Logger: c.logger, Action: "DeleteBucket", Bucket: &bucket})
|
||||
}
|
||||
|
||||
err = c.be.DeleteBucket(bucket)
|
||||
return SendResponse(ctx, err)
|
||||
return SendResponse(ctx, err, LogOptions{Logger: c.logger, Action: "DeleteBucket", Bucket: &bucket})
|
||||
}
|
||||
|
||||
func (c S3ApiController) DeleteObjects(ctx *fiber.Ctx) error {
|
||||
@@ -425,25 +562,25 @@ func (c S3ApiController) DeleteObjects(ctx *fiber.Ctx) error {
|
||||
var dObj types.Delete
|
||||
|
||||
if err := xml.Unmarshal(ctx.Body(), &dObj); err != nil {
|
||||
return errors.New("wrong api call")
|
||||
return SendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidRequest), LogOptions{Logger: c.logger, Action: "DeleteObjects", Bucket: &bucket})
|
||||
}
|
||||
|
||||
data, err := c.be.GetBucketAcl(bucket)
|
||||
if err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
return SendResponse(ctx, err, LogOptions{Logger: c.logger, Action: "DeleteObjects", Bucket: &bucket})
|
||||
}
|
||||
|
||||
parsedAcl, err := auth.ParseACL(data)
|
||||
if err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
return SendResponse(ctx, err, LogOptions{Logger: c.logger, Action: "DeleteObjects", Bucket: &bucket})
|
||||
}
|
||||
|
||||
if err := auth.VerifyACL(parsedAcl, bucket, access, "WRITE", isRoot); err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
return SendResponse(ctx, err, LogOptions{Logger: c.logger, Action: "DeleteObjects", Bucket: &bucket})
|
||||
}
|
||||
|
||||
err = c.be.DeleteObjects(bucket, &s3.DeleteObjectsInput{Delete: &dObj})
|
||||
return SendResponse(ctx, err)
|
||||
return SendResponse(ctx, err, LogOptions{Logger: c.logger, Action: "DeleteObjects", Bucket: &bucket})
|
||||
}
|
||||
|
||||
func (c S3ApiController) DeleteActions(ctx *fiber.Ctx) error {
|
||||
@@ -460,19 +597,28 @@ func (c S3ApiController) DeleteActions(ctx *fiber.Ctx) error {
|
||||
|
||||
data, err := c.be.GetBucketAcl(bucket)
|
||||
if err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
return SendResponse(ctx, err, LogOptions{Logger: c.logger, Bucket: &bucket, Object: &key})
|
||||
}
|
||||
|
||||
parsedAcl, err := auth.ParseACL(data)
|
||||
if err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
return SendResponse(ctx, err, LogOptions{Logger: c.logger, Bucket: &bucket, Object: &key})
|
||||
}
|
||||
|
||||
if ctx.Request().URI().QueryArgs().Has("tagging") {
|
||||
if err := auth.VerifyACL(parsedAcl, bucket, access, "WRITE", isRoot); err != nil {
|
||||
return SendResponse(ctx, err, LogOptions{Logger: c.logger, Action: "RemoveObjectTagging", Bucket: &bucket, Object: &key})
|
||||
}
|
||||
|
||||
err = c.be.RemoveTags(bucket, key)
|
||||
return SendResponse(ctx, err, LogOptions{Logger: c.logger, Action: "RemoveObjectTagging", Bucket: &bucket, Object: &key})
|
||||
}
|
||||
|
||||
if uploadId != "" {
|
||||
expectedBucketOwner, requestPayer := ctx.Get("X-Amz-Expected-Bucket-Owner"), ctx.Get("X-Amz-Request-Payer")
|
||||
|
||||
if err := auth.VerifyACL(parsedAcl, bucket, access, "WRITE", isRoot); err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
return SendResponse(ctx, err, LogOptions{Logger: c.logger, Action: "AbortMultipartUpload", Bucket: &bucket, Object: &key})
|
||||
}
|
||||
|
||||
err := c.be.AbortMultipartUpload(&s3.AbortMultipartUploadInput{
|
||||
@@ -482,15 +628,15 @@ func (c S3ApiController) DeleteActions(ctx *fiber.Ctx) error {
|
||||
ExpectedBucketOwner: &expectedBucketOwner,
|
||||
RequestPayer: types.RequestPayer(requestPayer),
|
||||
})
|
||||
return SendResponse(ctx, err)
|
||||
return SendResponse(ctx, err, LogOptions{Logger: c.logger, Action: "AbortMultipartUpload", Bucket: &bucket, Object: &key})
|
||||
}
|
||||
|
||||
if err := auth.VerifyACL(parsedAcl, bucket, access, "WRITE", isRoot); err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
return SendResponse(ctx, err, LogOptions{Logger: c.logger, Action: "DeleteObject", Bucket: &bucket, Object: &key})
|
||||
}
|
||||
|
||||
err = c.be.DeleteObject(bucket, key)
|
||||
return SendResponse(ctx, err)
|
||||
return SendResponse(ctx, err, LogOptions{Logger: c.logger, Action: "DeleteObject", Bucket: &bucket, Object: &key})
|
||||
}
|
||||
|
||||
func (c S3ApiController) HeadBucket(ctx *fiber.Ctx) error {
|
||||
@@ -498,21 +644,21 @@ func (c S3ApiController) HeadBucket(ctx *fiber.Ctx) error {
|
||||
|
||||
data, err := c.be.GetBucketAcl(bucket)
|
||||
if err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
return SendResponse(ctx, err, LogOptions{Logger: c.logger, Action: "HeadBucket", Bucket: &bucket})
|
||||
}
|
||||
|
||||
parsedAcl, err := auth.ParseACL(data)
|
||||
if err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
return SendResponse(ctx, err, LogOptions{Logger: c.logger, Action: "HeadBucket", Bucket: &bucket})
|
||||
}
|
||||
|
||||
if err := auth.VerifyACL(parsedAcl, bucket, access, "READ", isRoot); err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
return SendResponse(ctx, err, LogOptions{Logger: c.logger, Action: "HeadBucket", Bucket: &bucket})
|
||||
}
|
||||
|
||||
_, err = c.be.HeadBucket(bucket)
|
||||
// TODO: set bucket response headers
|
||||
return SendResponse(ctx, err)
|
||||
return SendResponse(ctx, err, LogOptions{Logger: c.logger, Action: "HeadBucket", Bucket: &bucket})
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -529,24 +675,24 @@ func (c S3ApiController) HeadObject(ctx *fiber.Ctx) error {
|
||||
|
||||
data, err := c.be.GetBucketAcl(bucket)
|
||||
if err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
return SendResponse(ctx, err, LogOptions{Logger: c.logger, Action: "HeadObject", Bucket: &bucket, Object: &key})
|
||||
}
|
||||
|
||||
parsedAcl, err := auth.ParseACL(data)
|
||||
if err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
return SendResponse(ctx, err, LogOptions{Logger: c.logger, Action: "HeadObject", Bucket: &bucket, Object: &key})
|
||||
}
|
||||
|
||||
if err := auth.VerifyACL(parsedAcl, bucket, access, "READ", isRoot); err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
return SendResponse(ctx, err, LogOptions{Logger: c.logger, Action: "HeadObject", Bucket: &bucket, Object: &key})
|
||||
}
|
||||
|
||||
res, err := c.be.HeadObject(bucket, key)
|
||||
if err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
return SendResponse(ctx, err, LogOptions{Logger: c.logger, Action: "HeadObject", Bucket: &bucket, Object: &key})
|
||||
}
|
||||
if res == nil {
|
||||
return SendResponse(ctx, fmt.Errorf("head object nil response"))
|
||||
return SendResponse(ctx, fmt.Errorf("head object nil response"), LogOptions{Logger: c.logger, Action: "HeadObject", Bucket: &bucket, Object: &key})
|
||||
}
|
||||
|
||||
utils.SetMetaHeaders(ctx, res.Metadata)
|
||||
@@ -585,7 +731,7 @@ func (c S3ApiController) HeadObject(ctx *fiber.Ctx) error {
|
||||
},
|
||||
})
|
||||
|
||||
return SendResponse(ctx, nil)
|
||||
return SendResponse(ctx, nil, LogOptions{Logger: c.logger, Action: "HeadObject", Bucket: &bucket, Object: &key})
|
||||
}
|
||||
|
||||
func (c S3ApiController) CreateActions(ctx *fiber.Ctx) error {
|
||||
@@ -602,27 +748,27 @@ func (c S3ApiController) CreateActions(ctx *fiber.Ctx) error {
|
||||
|
||||
data, err := c.be.GetBucketAcl(bucket)
|
||||
if err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
return SendResponse(ctx, err, LogOptions{Logger: c.logger, Bucket: &bucket, Object: &key})
|
||||
}
|
||||
|
||||
parsedAcl, err := auth.ParseACL(data)
|
||||
if err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
return SendResponse(ctx, err, LogOptions{Logger: c.logger, Bucket: &bucket, Object: &key})
|
||||
}
|
||||
|
||||
var restoreRequest s3.RestoreObjectInput
|
||||
if ctx.Request().URI().QueryArgs().Has("restore") {
|
||||
xmlErr := xml.Unmarshal(ctx.Body(), &restoreRequest)
|
||||
if xmlErr != nil {
|
||||
return errors.New("wrong api call")
|
||||
err := xml.Unmarshal(ctx.Body(), &restoreRequest)
|
||||
if err != nil {
|
||||
return SendResponse(ctx, err, LogOptions{Logger: c.logger, Action: "RestoreObject", Bucket: &bucket, Object: &key})
|
||||
}
|
||||
|
||||
if err := auth.VerifyACL(parsedAcl, bucket, access, "WRITE", isRoot); err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
return SendResponse(ctx, err, LogOptions{Logger: c.logger, Action: "RestoreObject", Bucket: &bucket, Object: &key})
|
||||
}
|
||||
|
||||
err := c.be.RestoreObject(bucket, key, &restoreRequest)
|
||||
return SendResponse(ctx, err)
|
||||
err = c.be.RestoreObject(bucket, key, &restoreRequest)
|
||||
return SendResponse(ctx, err, LogOptions{Logger: c.logger, Action: "RestoreObject", Bucket: &bucket, Object: &key})
|
||||
}
|
||||
|
||||
if uploadId != "" {
|
||||
@@ -631,27 +777,49 @@ func (c S3ApiController) CreateActions(ctx *fiber.Ctx) error {
|
||||
}{}
|
||||
|
||||
if err := xml.Unmarshal(ctx.Body(), &data); err != nil {
|
||||
return errors.New("wrong api call")
|
||||
return SendXMLResponse(ctx, nil, err, LogOptions{Logger: c.logger, Action: "CompleteMultipartUpload", Bucket: &bucket, Object: &key})
|
||||
}
|
||||
|
||||
if err := auth.VerifyACL(parsedAcl, bucket, access, "WRITE", isRoot); err != nil {
|
||||
return SendXMLResponse(ctx, nil, err)
|
||||
return SendXMLResponse(ctx, nil, err, LogOptions{Logger: c.logger, Action: "CompleteMultipartUpload", Bucket: &bucket, Object: &key})
|
||||
}
|
||||
|
||||
res, err := c.be.CompleteMultipartUpload(bucket, key, uploadId, data.Parts)
|
||||
return SendXMLResponse(ctx, res, err)
|
||||
return SendXMLResponse(ctx, res, err, LogOptions{Logger: c.logger, Action: "CompleteMultipartUpload", Bucket: &bucket, Object: &key})
|
||||
}
|
||||
|
||||
if err := auth.VerifyACL(parsedAcl, bucket, access, "WRITE", isRoot); err != nil {
|
||||
return SendXMLResponse(ctx, nil, err)
|
||||
return SendXMLResponse(ctx, nil, err, LogOptions{Logger: c.logger, Action: "CreateMultipartUpload", Bucket: &bucket, Object: &key})
|
||||
}
|
||||
|
||||
res, err := c.be.CreateMultipartUpload(&s3.CreateMultipartUploadInput{Bucket: &bucket, Key: &key})
|
||||
return SendXMLResponse(ctx, res, err)
|
||||
return SendXMLResponse(ctx, res, err, LogOptions{Logger: c.logger, Action: "CreateMultipartUpload", Bucket: &bucket, Object: &key})
|
||||
}
|
||||
|
||||
func SendResponse(ctx *fiber.Ctx, err error) error {
|
||||
type LogOptions struct {
|
||||
Logger s3log.Logger
|
||||
Action string
|
||||
Bucket *string
|
||||
Object *string
|
||||
LogType string
|
||||
}
|
||||
|
||||
func SendResponse(ctx *fiber.Ctx, err error, lo LogOptions) error {
|
||||
if err != nil {
|
||||
if lo.Logger != nil {
|
||||
var access *string
|
||||
acc := ctx.Locals("access")
|
||||
switch tp := acc.(type) {
|
||||
case string:
|
||||
access = &tp
|
||||
}
|
||||
|
||||
if lo.LogType == "auth" {
|
||||
lo.Logger.SendAuthLog(access, err)
|
||||
} else {
|
||||
lo.Logger.SendErrorLog(err, lo.Action, access, lo.Bucket, lo.Object)
|
||||
}
|
||||
}
|
||||
serr, ok := err.(s3err.APIError)
|
||||
if ok {
|
||||
ctx.Status(serr.HTTPStatusCode)
|
||||
@@ -666,14 +834,33 @@ func SendResponse(ctx *fiber.Ctx, err error) error {
|
||||
|
||||
utils.LogCtxDetails(ctx, []byte{})
|
||||
|
||||
if lo.Logger != nil {
|
||||
var access *string
|
||||
acc := ctx.Locals("access")
|
||||
switch tp := acc.(type) {
|
||||
case string:
|
||||
access = &tp
|
||||
}
|
||||
|
||||
if lo.LogType == "auth" {
|
||||
lo.Logger.SendAuthLog(access, nil)
|
||||
} else {
|
||||
lo.Logger.SendSuccessLog(nil, lo.Action, access, lo.Bucket, lo.Object)
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/gofiber/fiber/issues/2080
|
||||
// ctx.SendStatus() sets incorrect content length on HEAD request
|
||||
ctx.Status(http.StatusOK)
|
||||
return nil
|
||||
}
|
||||
|
||||
func SendXMLResponse(ctx *fiber.Ctx, resp any, err error) error {
|
||||
func SendXMLResponse(ctx *fiber.Ctx, resp any, err error, lo LogOptions) error {
|
||||
if err != nil {
|
||||
if lo.Logger != nil {
|
||||
access := ctx.Locals("access").(string)
|
||||
lo.Logger.SendErrorLog(err, lo.Action, &access, lo.Bucket, lo.Object)
|
||||
}
|
||||
serr, ok := err.(s3err.APIError)
|
||||
if ok {
|
||||
ctx.Status(serr.HTTPStatusCode)
|
||||
@@ -700,6 +887,12 @@ func SendXMLResponse(ctx *fiber.Ctx, resp any, err error) error {
|
||||
}
|
||||
|
||||
utils.LogCtxDetails(ctx, b)
|
||||
if lo.Logger != nil {
|
||||
access := ctx.Locals("access").(string)
|
||||
if lo.Logger != nil {
|
||||
lo.Logger.SendSuccessLog(resp, lo.Action, &access, lo.Bucket, lo.Object)
|
||||
}
|
||||
}
|
||||
|
||||
return ctx.Send(b)
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -30,6 +31,7 @@ import (
|
||||
"github.com/versity/versitygw/auth"
|
||||
"github.com/versity/versitygw/backend"
|
||||
"github.com/versity/versitygw/s3err"
|
||||
"github.com/versity/versitygw/s3log"
|
||||
"github.com/versity/versitygw/s3response"
|
||||
)
|
||||
|
||||
@@ -48,7 +50,9 @@ func init() {
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
type args struct {
|
||||
be backend.Backend
|
||||
be backend.Backend
|
||||
iam auth.IAMService
|
||||
logger s3log.Logger
|
||||
}
|
||||
|
||||
be := backend.BackendUnsupported{}
|
||||
@@ -61,16 +65,18 @@ func TestNew(t *testing.T) {
|
||||
{
|
||||
name: "Initialize S3 api controller",
|
||||
args: args{
|
||||
be: be,
|
||||
be: be,
|
||||
iam: &auth.IAMServiceInternal{},
|
||||
},
|
||||
want: S3ApiController{
|
||||
be: be,
|
||||
be: be,
|
||||
iam: &auth.IAMServiceInternal{},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := New(tt.args.be); !reflect.DeepEqual(got, tt.want) {
|
||||
if got := New(tt.args.be, tt.args.iam, tt.args.logger); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("New() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
@@ -123,6 +129,16 @@ func TestS3ApiController_ListBuckets(t *testing.T) {
|
||||
})
|
||||
appErr.Get("/", s3ApiControllerErr.ListBuckets)
|
||||
|
||||
//Admin error case
|
||||
admErr := fiber.New()
|
||||
admErr.Use(func(ctx *fiber.Ctx) error {
|
||||
ctx.Locals("access", "valid access")
|
||||
ctx.Locals("isRoot", false)
|
||||
ctx.Locals("isDebug", false)
|
||||
return ctx.Next()
|
||||
})
|
||||
admErr.Get("/", s3ApiController.ListBuckets)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
@@ -148,6 +164,15 @@ func TestS3ApiController_ListBuckets(t *testing.T) {
|
||||
wantErr: false,
|
||||
statusCode: 200,
|
||||
},
|
||||
{
|
||||
name: "admin-error-case",
|
||||
args: args{
|
||||
req: httptest.NewRequest(http.MethodGet, "/", nil),
|
||||
},
|
||||
app: admErr,
|
||||
wantErr: false,
|
||||
statusCode: 500,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@@ -164,11 +189,17 @@ func TestS3ApiController_ListBuckets(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func getPtr(val string) *string {
|
||||
return &val
|
||||
}
|
||||
|
||||
func TestS3ApiController_GetActions(t *testing.T) {
|
||||
type args struct {
|
||||
req *http.Request
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
app := fiber.New()
|
||||
s3ApiController := S3ApiController{
|
||||
be: &BackendMock{
|
||||
@@ -185,7 +216,18 @@ func TestS3ApiController_GetActions(t *testing.T) {
|
||||
return &s3.GetObjectAttributesOutput{}, nil
|
||||
},
|
||||
GetObjectFunc: func(bucket, object, acceptRange string, writer io.Writer) (*s3.GetObjectOutput, error) {
|
||||
return &s3.GetObjectOutput{Metadata: nil}, nil
|
||||
return &s3.GetObjectOutput{
|
||||
Metadata: map[string]string{"hello": "world"},
|
||||
ContentType: getPtr("application/xml"),
|
||||
ContentEncoding: getPtr("gzip"),
|
||||
ETag: getPtr("98sda7f97sa9df798sd79f8as9df"),
|
||||
ContentLength: 1000,
|
||||
LastModified: &now,
|
||||
StorageClass: "storage class",
|
||||
}, nil
|
||||
},
|
||||
GetTagsFunc: func(bucket, object string) (map[string]string, error) {
|
||||
return map[string]string{"hello": "world"}, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -197,17 +239,9 @@ func TestS3ApiController_GetActions(t *testing.T) {
|
||||
})
|
||||
app.Get("/:bucket/:key/*", s3ApiController.GetActions)
|
||||
|
||||
// GetObjectACL
|
||||
getObjectACLReq := httptest.NewRequest(http.MethodGet, "/my-bucket/key", nil)
|
||||
getObjectACLReq.Header.Set("X-Amz-Object-Attributes", "attrs")
|
||||
|
||||
// GetObject error case
|
||||
getObjectReq := httptest.NewRequest(http.MethodGet, "/my-bucket/key", nil)
|
||||
getObjectReq.Header.Set("Range", "hello=")
|
||||
|
||||
// GetObject success case
|
||||
getObjectSuccessReq := httptest.NewRequest(http.MethodGet, "/my-bucket/key", nil)
|
||||
getObjectReq.Header.Set("Range", "range=13-invalid")
|
||||
// GetObjectAttributes success case
|
||||
getObjAttrs := httptest.NewRequest(http.MethodGet, "/my-bucket/key", nil)
|
||||
getObjAttrs.Header.Set("X-Amz-Object-Attributes", "hello")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -217,19 +251,46 @@ func TestS3ApiController_GetActions(t *testing.T) {
|
||||
statusCode int
|
||||
}{
|
||||
{
|
||||
name: "Get-actions-invalid-max-parts",
|
||||
name: "Get-actions-get-tags-success",
|
||||
app: app,
|
||||
args: args{
|
||||
req: httptest.NewRequest(http.MethodGet, "/my-bucket/key?uploadId=hello&max-parts=InvalidMaxParts", nil),
|
||||
req: httptest.NewRequest(http.MethodGet, "/my-bucket/key/key.json?tagging", nil),
|
||||
},
|
||||
wantErr: false,
|
||||
statusCode: 200,
|
||||
},
|
||||
{
|
||||
name: "Get-actions-invalid-max-parts-string",
|
||||
app: app,
|
||||
args: args{
|
||||
req: httptest.NewRequest(http.MethodGet, "/my-bucket/key?uploadId=hello&max-parts=invalid", nil),
|
||||
},
|
||||
wantErr: false,
|
||||
statusCode: 400,
|
||||
},
|
||||
{
|
||||
name: "Get-actions-invalid-part-number-marker",
|
||||
name: "Get-actions-invalid-max-parts-negative",
|
||||
app: app,
|
||||
args: args{
|
||||
req: httptest.NewRequest(http.MethodGet, "/my-bucket/key?uploadId=hello&max-parts=200&part-number-marker=InvalidPartNumber", nil),
|
||||
req: httptest.NewRequest(http.MethodGet, "/my-bucket/key?uploadId=hello&max-parts=-8", nil),
|
||||
},
|
||||
wantErr: false,
|
||||
statusCode: 400,
|
||||
},
|
||||
{
|
||||
name: "Get-actions-invalid-part-number-marker-string",
|
||||
app: app,
|
||||
args: args{
|
||||
req: httptest.NewRequest(http.MethodGet, "/my-bucket/key?uploadId=hello&max-parts=200&part-number-marker=invalid", nil),
|
||||
},
|
||||
wantErr: false,
|
||||
statusCode: 400,
|
||||
},
|
||||
{
|
||||
name: "Get-actions-invalid-part-number-marker-negative",
|
||||
app: app,
|
||||
args: args{
|
||||
req: httptest.NewRequest(http.MethodGet, "/my-bucket/key?uploadId=hello&max-parts=200&part-number-marker=-8", nil),
|
||||
},
|
||||
wantErr: false,
|
||||
statusCode: 400,
|
||||
@@ -247,7 +308,16 @@ func TestS3ApiController_GetActions(t *testing.T) {
|
||||
name: "Get-actions-get-object-acl-success",
|
||||
app: app,
|
||||
args: args{
|
||||
req: getObjectACLReq,
|
||||
req: httptest.NewRequest(http.MethodGet, "/my-bucket/key?acl", nil),
|
||||
},
|
||||
wantErr: false,
|
||||
statusCode: 200,
|
||||
},
|
||||
{
|
||||
name: "Get-actions-get-object-attributes-success",
|
||||
app: app,
|
||||
args: args{
|
||||
req: getObjAttrs,
|
||||
},
|
||||
wantErr: false,
|
||||
statusCode: 200,
|
||||
@@ -256,7 +326,7 @@ func TestS3ApiController_GetActions(t *testing.T) {
|
||||
name: "Get-actions-get-object-success",
|
||||
app: app,
|
||||
args: args{
|
||||
req: getObjectSuccessReq,
|
||||
req: httptest.NewRequest(http.MethodGet, "/my-bucket/key", nil),
|
||||
},
|
||||
wantErr: false,
|
||||
statusCode: 200,
|
||||
@@ -387,11 +457,11 @@ func TestS3ApiController_ListActions(t *testing.T) {
|
||||
resp, err := tt.app.Test(tt.args.req)
|
||||
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("S3ApiController.GetActions() error = %v, wantErr %v", err, tt.wantErr)
|
||||
t.Errorf("S3ApiController.ListActions() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
|
||||
if resp.StatusCode != tt.statusCode {
|
||||
t.Errorf("S3ApiController.GetActions() statusCode = %v, wantStatusCode = %v", resp.StatusCode, tt.statusCode)
|
||||
t.Errorf("S3ApiController.ListActions() statusCode = %v, wantStatusCode = %v", resp.StatusCode, tt.statusCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -403,6 +473,31 @@ func TestS3ApiController_PutBucketActions(t *testing.T) {
|
||||
}
|
||||
|
||||
app := fiber.New()
|
||||
|
||||
// Mock valid acl
|
||||
acl := auth.ACL{Owner: "valid access", ACL: "public-read-write"}
|
||||
acldata, err := json.Marshal(acl)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to parse the params: %v", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
body := `
|
||||
<AccessControlPolicy xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
|
||||
<AccessControlList>
|
||||
<Grant>
|
||||
<Grantee>
|
||||
<ID>hell</ID>
|
||||
</Grantee>
|
||||
<Permission>string</Permission>
|
||||
</Grant>
|
||||
</AccessControlList>
|
||||
<Owner>
|
||||
<ID>hello</ID>
|
||||
</Owner>
|
||||
</AccessControlPolicy>
|
||||
`
|
||||
|
||||
s3ApiController := S3ApiController{
|
||||
be: &BackendMock{
|
||||
GetBucketAclFunc: func(bucket string) ([]byte, error) {
|
||||
@@ -425,14 +520,28 @@ func TestS3ApiController_PutBucketActions(t *testing.T) {
|
||||
})
|
||||
app.Put("/:bucket", s3ApiController.PutBucketActions)
|
||||
|
||||
// Error case
|
||||
errorReq := httptest.NewRequest(http.MethodPut, "/my-bucket", nil)
|
||||
errorReq.Header.Set("X-Amz-Acl", "restricted")
|
||||
errorReq.Header.Set("X-Amz-Grant-Read", "read")
|
||||
// invalid acl case
|
||||
invAclReq := httptest.NewRequest(http.MethodPut, "/my-bucket?acl", nil)
|
||||
invAclReq.Header.Set("X-Amz-Acl", "invalid")
|
||||
|
||||
// PutBucketAcl success
|
||||
aclReq := httptest.NewRequest(http.MethodPut, "/my-bucket", nil)
|
||||
errorReq.Header.Set("X-Amz-Acl", "full")
|
||||
// invalid acl case 2
|
||||
errAclReq := httptest.NewRequest(http.MethodPut, "/my-bucket?acl", nil)
|
||||
errAclReq.Header.Set("X-Amz-Acl", "private")
|
||||
errAclReq.Header.Set("X-Amz-Grant-Read", "hello")
|
||||
|
||||
// PutBucketAcl incorrect bucket owner case
|
||||
incorrectBucketOwner := httptest.NewRequest(http.MethodPut, "/my-bucket?acl", nil)
|
||||
incorrectBucketOwner.Header.Set("X-Amz-Acl", "private")
|
||||
incorrectBucketOwner.Header.Set("X-Amz-Expected-Bucket-Owner", "invalid access")
|
||||
|
||||
// PutBucketAcl acl success
|
||||
aclSuccReq := httptest.NewRequest(http.MethodPut, "/my-bucket?acl", nil)
|
||||
aclSuccReq.Header.Set("X-Amz-Acl", "private")
|
||||
aclSuccReq.Header.Set("X-Amz-Expected-Bucket-Owner", "valid access")
|
||||
|
||||
// Invalid acl body case
|
||||
errAclBodyReq := httptest.NewRequest(http.MethodPut, "/my-bucket?acl", strings.NewReader(body))
|
||||
errAclBodyReq.Header.Set("X-Amz-Grant-Read", "hello")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -442,19 +551,46 @@ func TestS3ApiController_PutBucketActions(t *testing.T) {
|
||||
statusCode int
|
||||
}{
|
||||
{
|
||||
name: "Put-bucket-acl-error",
|
||||
name: "Put-bucket-acl-invalid-acl",
|
||||
app: app,
|
||||
args: args{
|
||||
req: errorReq,
|
||||
req: invAclReq,
|
||||
},
|
||||
wantErr: false,
|
||||
statusCode: 500,
|
||||
statusCode: 400,
|
||||
},
|
||||
{
|
||||
name: "Put-object-acl-success",
|
||||
name: "Put-bucket-acl-incorrect-acl",
|
||||
app: app,
|
||||
args: args{
|
||||
req: aclReq,
|
||||
req: errAclReq,
|
||||
},
|
||||
wantErr: false,
|
||||
statusCode: 400,
|
||||
},
|
||||
{
|
||||
name: "Put-bucket-acl-incorrect-acl-body",
|
||||
app: app,
|
||||
args: args{
|
||||
req: errAclBodyReq,
|
||||
},
|
||||
wantErr: false,
|
||||
statusCode: 400,
|
||||
},
|
||||
{
|
||||
name: "Put-bucket-acl-incorrect-bucket-owner",
|
||||
app: app,
|
||||
args: args{
|
||||
req: incorrectBucketOwner,
|
||||
},
|
||||
wantErr: false,
|
||||
statusCode: 403,
|
||||
},
|
||||
{
|
||||
name: "Put-bucket-acl-success",
|
||||
app: app,
|
||||
args: args{
|
||||
req: aclSuccReq,
|
||||
},
|
||||
wantErr: false,
|
||||
statusCode: 200,
|
||||
@@ -473,11 +609,11 @@ func TestS3ApiController_PutBucketActions(t *testing.T) {
|
||||
resp, err := tt.app.Test(tt.args.req)
|
||||
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("S3ApiController.GetActions() error = %v, wantErr %v", err, tt.wantErr)
|
||||
t.Errorf("S3ApiController.PutBucketActions() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
|
||||
if resp.StatusCode != tt.statusCode {
|
||||
t.Errorf("S3ApiController.GetActions() statusCode = %v, wantStatusCode = %v", resp.StatusCode, tt.statusCode)
|
||||
t.Errorf("S3ApiController.PutBucketActions() statusCode = %v, wantStatusCode = %v", resp.StatusCode, tt.statusCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -487,15 +623,38 @@ func TestS3ApiController_PutActions(t *testing.T) {
|
||||
req *http.Request
|
||||
}
|
||||
|
||||
body := `
|
||||
<AccessControlPolicy xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
|
||||
<AccessControlList>
|
||||
<Grant>
|
||||
<Grantee>
|
||||
<ID>hell</ID>
|
||||
</Grantee>
|
||||
<Permission>string</Permission>
|
||||
</Grant>
|
||||
</AccessControlList>
|
||||
<Owner>
|
||||
<ID>hello</ID>
|
||||
</Owner>
|
||||
</AccessControlPolicy>
|
||||
`
|
||||
tagBody := `
|
||||
<Tagging xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
|
||||
<TagSet>
|
||||
<Tag>
|
||||
<Key>string</Key>
|
||||
<Value>string</Value>
|
||||
</Tag>
|
||||
</TagSet>
|
||||
</Tagging>
|
||||
`
|
||||
|
||||
app := fiber.New()
|
||||
s3ApiController := S3ApiController{
|
||||
be: &BackendMock{
|
||||
GetBucketAclFunc: func(bucket string) ([]byte, error) {
|
||||
return acldata, nil
|
||||
},
|
||||
UploadPartCopyFunc: func(*s3.UploadPartCopyInput) (*s3.UploadPartCopyOutput, error) {
|
||||
return &s3.UploadPartCopyOutput{}, nil
|
||||
},
|
||||
PutObjectAclFunc: func(*s3.PutObjectAclInput) error {
|
||||
return nil
|
||||
},
|
||||
@@ -505,6 +664,15 @@ func TestS3ApiController_PutActions(t *testing.T) {
|
||||
PutObjectFunc: func(*s3.PutObjectInput) (string, error) {
|
||||
return "Hey", nil
|
||||
},
|
||||
PutObjectPartFunc: func(bucket, object, uploadID string, part int, length int64, r io.Reader) (string, error) {
|
||||
return "hello", nil
|
||||
},
|
||||
SetTagsFunc: func(bucket, object string, tags map[string]string) error {
|
||||
return nil
|
||||
},
|
||||
UploadPartCopyFunc: func(uploadPartCopyInput *s3.UploadPartCopyInput) (s3response.CopyObjectResult, error) {
|
||||
return s3response.CopyObjectResult{}, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
app.Use(func(ctx *fiber.Ctx) error {
|
||||
@@ -515,19 +683,39 @@ func TestS3ApiController_PutActions(t *testing.T) {
|
||||
})
|
||||
app.Put("/:bucket/:key/*", s3ApiController.PutActions)
|
||||
|
||||
//PutObjectAcl error
|
||||
aclReqErr := httptest.NewRequest(http.MethodPut, "/my-bucket/my-key", nil)
|
||||
aclReqErr.Header.Set("X-Amz-Acl", "acl")
|
||||
aclReqErr.Header.Set("X-Amz-Grant-Write", "write")
|
||||
// UploadPartCopy success
|
||||
uploadPartCpyReq := httptest.NewRequest(http.MethodPut, "/my-bucket/my-key?uploadId=12asd32&partNumber=3", nil)
|
||||
uploadPartCpyReq.Header.Set("X-Amz-Copy-Source", "srcBucket/srcObject")
|
||||
|
||||
//PutObjectAcl success
|
||||
aclReq := httptest.NewRequest(http.MethodPut, "/my-bucket/my-key", nil)
|
||||
aclReq.Header.Set("X-Amz-Acl", "acl")
|
||||
// UploadPartCopy error case
|
||||
uploadPartCpyErrReq := httptest.NewRequest(http.MethodPut, "/my-bucket/my-key?uploadId=12asd32&partNumber=invalid", nil)
|
||||
uploadPartCpyErrReq.Header.Set("X-Amz-Copy-Source", "srcBucket/srcObject")
|
||||
|
||||
//CopyObject success
|
||||
// CopyObject success
|
||||
cpySrcReq := httptest.NewRequest(http.MethodPut, "/my-bucket/my-key", nil)
|
||||
cpySrcReq.Header.Set("X-Amz-Copy-Source", "srcBucket/srcObject")
|
||||
|
||||
// PutObjectAcl success
|
||||
aclReq := httptest.NewRequest(http.MethodPut, "/my-bucket/my-key", nil)
|
||||
aclReq.Header.Set("X-Amz-Acl", "private")
|
||||
|
||||
// PutObjectAcl success grt case
|
||||
aclGrtReq := httptest.NewRequest(http.MethodPut, "/my-bucket/my-key", nil)
|
||||
aclGrtReq.Header.Set("X-Amz-Grant-Read", "private")
|
||||
|
||||
// invalid acl case 1
|
||||
invAclReq := httptest.NewRequest(http.MethodPut, "/my-bucket/my-key?acl", nil)
|
||||
invAclReq.Header.Set("X-Amz-Acl", "invalid")
|
||||
|
||||
// invalid acl case 2
|
||||
errAclReq := httptest.NewRequest(http.MethodPut, "/my-bucket/my-key?acl", nil)
|
||||
errAclReq.Header.Set("X-Amz-Acl", "private")
|
||||
errAclReq.Header.Set("X-Amz-Grant-Read", "hello")
|
||||
|
||||
// invalid body & grt case
|
||||
invAclBodyGrtReq := httptest.NewRequest(http.MethodPut, "/my-bucket/my-key?acl", strings.NewReader(body))
|
||||
invAclBodyGrtReq.Header.Set("X-Amz-Grant-Read", "hello")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
app *fiber.App
|
||||
@@ -536,7 +724,7 @@ func TestS3ApiController_PutActions(t *testing.T) {
|
||||
statusCode int
|
||||
}{
|
||||
{
|
||||
name: "Upload-put-part-error-case",
|
||||
name: "Put-object-part-error-case",
|
||||
app: app,
|
||||
args: args{
|
||||
req: httptest.NewRequest(http.MethodPut, "/my-bucket/my-key?uploadId=abc&partNumber=invalid", nil),
|
||||
@@ -545,40 +733,49 @@ func TestS3ApiController_PutActions(t *testing.T) {
|
||||
statusCode: 400,
|
||||
},
|
||||
{
|
||||
name: "Upload-copy-part-success",
|
||||
name: "Put-object-part-success",
|
||||
app: app,
|
||||
args: args{
|
||||
req: httptest.NewRequest(http.MethodPut, "/my-bucket/my-key?partNumber=3", nil),
|
||||
req: httptest.NewRequest(http.MethodPut, "/my-bucket/my-key?uploadId=4&partNumber=3", nil),
|
||||
},
|
||||
wantErr: false,
|
||||
statusCode: 200,
|
||||
},
|
||||
{
|
||||
name: "Upload-part-success",
|
||||
name: "Set-tags-success",
|
||||
app: app,
|
||||
args: args{
|
||||
req: httptest.NewRequest(http.MethodPut, "/my-bucket/my-key?uploadId=234234", nil),
|
||||
req: httptest.NewRequest(http.MethodPut, "/my-bucket/my-key?tagging", strings.NewReader(tagBody)),
|
||||
},
|
||||
wantErr: false,
|
||||
statusCode: 200,
|
||||
},
|
||||
{
|
||||
name: "Put-object-acl-error",
|
||||
name: "Put-object-acl-invalid-acl",
|
||||
app: app,
|
||||
args: args{
|
||||
req: aclReqErr,
|
||||
req: invAclReq,
|
||||
},
|
||||
wantErr: false,
|
||||
statusCode: 500,
|
||||
statusCode: 400,
|
||||
},
|
||||
{
|
||||
name: "Put-object-acl-error",
|
||||
name: "Put-object-acl-incorrect-acl",
|
||||
app: app,
|
||||
args: args{
|
||||
req: aclReqErr,
|
||||
req: errAclReq,
|
||||
},
|
||||
wantErr: false,
|
||||
statusCode: 500,
|
||||
statusCode: 400,
|
||||
},
|
||||
{
|
||||
name: "Put-object-acl-incorrect-acl-body-case",
|
||||
app: app,
|
||||
args: args{
|
||||
req: invAclBodyGrtReq,
|
||||
},
|
||||
wantErr: false,
|
||||
statusCode: 400,
|
||||
},
|
||||
{
|
||||
name: "Put-object-acl-success",
|
||||
@@ -589,6 +786,42 @@ func TestS3ApiController_PutActions(t *testing.T) {
|
||||
wantErr: false,
|
||||
statusCode: 200,
|
||||
},
|
||||
{
|
||||
name: "Put-object-acl-success-body-case",
|
||||
app: app,
|
||||
args: args{
|
||||
req: httptest.NewRequest(http.MethodPut, "/my-bucket/my-key?acl", strings.NewReader(body)),
|
||||
},
|
||||
wantErr: false,
|
||||
statusCode: 200,
|
||||
},
|
||||
{
|
||||
name: "Put-object-acl-success-grt-case",
|
||||
app: app,
|
||||
args: args{
|
||||
req: aclGrtReq,
|
||||
},
|
||||
wantErr: false,
|
||||
statusCode: 200,
|
||||
},
|
||||
{
|
||||
name: "Upload-part-copy-invalid-part-number",
|
||||
app: app,
|
||||
args: args{
|
||||
req: uploadPartCpyErrReq,
|
||||
},
|
||||
wantErr: false,
|
||||
statusCode: 400,
|
||||
},
|
||||
{
|
||||
name: "Upload-part-copy-success",
|
||||
app: app,
|
||||
args: args{
|
||||
req: uploadPartCpyReq,
|
||||
},
|
||||
wantErr: false,
|
||||
statusCode: 200,
|
||||
},
|
||||
{
|
||||
name: "Copy-object-success",
|
||||
app: app,
|
||||
@@ -602,7 +835,7 @@ func TestS3ApiController_PutActions(t *testing.T) {
|
||||
name: "Put-object-success",
|
||||
app: app,
|
||||
args: args{
|
||||
req: httptest.NewRequest(http.MethodPut, "/my-bucket/my-key", nil),
|
||||
req: httptest.NewRequest(http.MethodPut, "/my-bucket/my-key/key2", nil),
|
||||
},
|
||||
wantErr: false,
|
||||
statusCode: 200,
|
||||
@@ -649,28 +882,6 @@ func TestS3ApiController_DeleteBucket(t *testing.T) {
|
||||
|
||||
app.Delete("/:bucket", s3ApiController.DeleteBucket)
|
||||
|
||||
// error case
|
||||
appErr := fiber.New()
|
||||
|
||||
s3ApiControllerErr := S3ApiController{
|
||||
be: &BackendMock{
|
||||
GetBucketAclFunc: func(bucket string) ([]byte, error) {
|
||||
return acldata, nil
|
||||
},
|
||||
DeleteBucketFunc: func(bucket string) error {
|
||||
return s3err.GetAPIError(48)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
appErr.Use(func(ctx *fiber.Ctx) error {
|
||||
ctx.Locals("access", "valid access")
|
||||
ctx.Locals("isRoot", true)
|
||||
ctx.Locals("isDebug", false)
|
||||
return ctx.Next()
|
||||
})
|
||||
appErr.Delete("/:bucket", s3ApiControllerErr.DeleteBucket)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
app *fiber.App
|
||||
@@ -687,15 +898,6 @@ func TestS3ApiController_DeleteBucket(t *testing.T) {
|
||||
wantErr: false,
|
||||
statusCode: 200,
|
||||
},
|
||||
{
|
||||
name: "Delete-bucket-error",
|
||||
app: appErr,
|
||||
args: args{
|
||||
req: httptest.NewRequest(http.MethodDelete, "/my-bucket", nil),
|
||||
},
|
||||
wantErr: false,
|
||||
statusCode: 400,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
resp, err := tt.app.Test(tt.args.req)
|
||||
@@ -764,7 +966,7 @@ func TestS3ApiController_DeleteObjects(t *testing.T) {
|
||||
req: httptest.NewRequest(http.MethodPost, "/my-bucket", nil),
|
||||
},
|
||||
wantErr: false,
|
||||
statusCode: 500,
|
||||
statusCode: 400,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
@@ -797,6 +999,9 @@ func TestS3ApiController_DeleteActions(t *testing.T) {
|
||||
AbortMultipartUploadFunc: func(*s3.AbortMultipartUploadInput) error {
|
||||
return nil
|
||||
},
|
||||
RemoveTagsFunc: func(bucket, object string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -808,7 +1013,7 @@ func TestS3ApiController_DeleteActions(t *testing.T) {
|
||||
})
|
||||
app.Delete("/:bucket/:key/*", s3ApiController.DeleteActions)
|
||||
|
||||
//Error case
|
||||
// Error case
|
||||
appErr := fiber.New()
|
||||
|
||||
s3ApiControllerErr := S3ApiController{be: &BackendMock{
|
||||
@@ -826,7 +1031,7 @@ func TestS3ApiController_DeleteActions(t *testing.T) {
|
||||
ctx.Locals("isDebug", false)
|
||||
return ctx.Next()
|
||||
})
|
||||
appErr.Delete("/:bucket", s3ApiControllerErr.DeleteBucket)
|
||||
appErr.Delete("/:bucket/:key/*", s3ApiControllerErr.DeleteActions)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -844,6 +1049,15 @@ func TestS3ApiController_DeleteActions(t *testing.T) {
|
||||
wantErr: false,
|
||||
statusCode: 200,
|
||||
},
|
||||
{
|
||||
name: "Remove-object-tagging-success",
|
||||
app: app,
|
||||
args: args{
|
||||
req: httptest.NewRequest(http.MethodDelete, "/my-bucket/my-key/key2?tagging", nil),
|
||||
},
|
||||
wantErr: false,
|
||||
statusCode: 200,
|
||||
},
|
||||
{
|
||||
name: "Delete-object-success",
|
||||
app: app,
|
||||
@@ -1148,6 +1362,8 @@ func TestS3ApiController_CreateActions(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
resp, err := tt.app.Test(tt.args.req)
|
||||
|
||||
fmt.Println(tt.name)
|
||||
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("S3ApiController.CreateActions() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
@@ -1224,7 +1440,7 @@ func Test_XMLresponse(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := SendXMLResponse(tt.args.ctx, tt.args.resp, tt.args.err); (err != nil) != tt.wantErr {
|
||||
if err := SendXMLResponse(tt.args.ctx, tt.args.resp, tt.args.err, LogOptions{}); (err != nil) != tt.wantErr {
|
||||
t.Errorf("response() %v error = %v, wantErr %v", tt.name, err, tt.wantErr)
|
||||
}
|
||||
|
||||
@@ -1271,6 +1487,16 @@ func Test_response(t *testing.T) {
|
||||
wantErr: false,
|
||||
statusCode: 500,
|
||||
},
|
||||
{
|
||||
name: "Internal-server-error-not-api",
|
||||
args: args{
|
||||
ctx: &ctx,
|
||||
resp: nil,
|
||||
err: fmt.Errorf("custom error"),
|
||||
},
|
||||
wantErr: false,
|
||||
statusCode: 500,
|
||||
},
|
||||
{
|
||||
name: "Error-not-implemented",
|
||||
args: args{
|
||||
@@ -1294,7 +1520,7 @@ func Test_response(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := SendResponse(tt.args.ctx, tt.args.err); (err != nil) != tt.wantErr {
|
||||
if err := SendResponse(tt.args.ctx, tt.args.err, LogOptions{}); (err != nil) != tt.wantErr {
|
||||
t.Errorf("response() %v error = %v, wantErr %v", tt.name, err, tt.wantErr)
|
||||
}
|
||||
|
||||
|
||||
169
s3api/controllers/iam_moq_test.go
Normal file
169
s3api/controllers/iam_moq_test.go
Normal file
@@ -0,0 +1,169 @@
|
||||
// Code generated by moq; DO NOT EDIT.
|
||||
// github.com/matryer/moq
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"github.com/versity/versitygw/auth"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Ensure, that IAMServiceMock does implement auth.IAMService.
|
||||
// If this is not the case, regenerate this file with moq.
|
||||
var _ auth.IAMService = &IAMServiceMock{}
|
||||
|
||||
// IAMServiceMock is a mock implementation of auth.IAMService.
|
||||
//
|
||||
// func TestSomethingThatUsesIAMService(t *testing.T) {
|
||||
//
|
||||
// // make and configure a mocked auth.IAMService
|
||||
// mockedIAMService := &IAMServiceMock{
|
||||
// CreateAccountFunc: func(access string, account auth.Account) error {
|
||||
// panic("mock out the CreateAccount method")
|
||||
// },
|
||||
// DeleteUserAccountFunc: func(access string) error {
|
||||
// panic("mock out the DeleteUserAccount method")
|
||||
// },
|
||||
// GetUserAccountFunc: func(access string) (auth.Account, error) {
|
||||
// panic("mock out the GetUserAccount method")
|
||||
// },
|
||||
// }
|
||||
//
|
||||
// // use mockedIAMService in code that requires auth.IAMService
|
||||
// // and then make assertions.
|
||||
//
|
||||
// }
|
||||
type IAMServiceMock struct {
|
||||
// CreateAccountFunc mocks the CreateAccount method.
|
||||
CreateAccountFunc func(access string, account auth.Account) error
|
||||
|
||||
// DeleteUserAccountFunc mocks the DeleteUserAccount method.
|
||||
DeleteUserAccountFunc func(access string) error
|
||||
|
||||
// GetUserAccountFunc mocks the GetUserAccount method.
|
||||
GetUserAccountFunc func(access string) (auth.Account, error)
|
||||
|
||||
// calls tracks calls to the methods.
|
||||
calls struct {
|
||||
// CreateAccount holds details about calls to the CreateAccount method.
|
||||
CreateAccount []struct {
|
||||
// Access is the access argument value.
|
||||
Access string
|
||||
// Account is the account argument value.
|
||||
Account auth.Account
|
||||
}
|
||||
// DeleteUserAccount holds details about calls to the DeleteUserAccount method.
|
||||
DeleteUserAccount []struct {
|
||||
// Access is the access argument value.
|
||||
Access string
|
||||
}
|
||||
// GetUserAccount holds details about calls to the GetUserAccount method.
|
||||
GetUserAccount []struct {
|
||||
// Access is the access argument value.
|
||||
Access string
|
||||
}
|
||||
}
|
||||
lockCreateAccount sync.RWMutex
|
||||
lockDeleteUserAccount sync.RWMutex
|
||||
lockGetUserAccount sync.RWMutex
|
||||
}
|
||||
|
||||
// CreateAccount calls CreateAccountFunc.
|
||||
func (mock *IAMServiceMock) CreateAccount(access string, account auth.Account) error {
|
||||
if mock.CreateAccountFunc == nil {
|
||||
panic("IAMServiceMock.CreateAccountFunc: method is nil but IAMService.CreateAccount was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
Access string
|
||||
Account auth.Account
|
||||
}{
|
||||
Access: access,
|
||||
Account: account,
|
||||
}
|
||||
mock.lockCreateAccount.Lock()
|
||||
mock.calls.CreateAccount = append(mock.calls.CreateAccount, callInfo)
|
||||
mock.lockCreateAccount.Unlock()
|
||||
return mock.CreateAccountFunc(access, account)
|
||||
}
|
||||
|
||||
// CreateAccountCalls gets all the calls that were made to CreateAccount.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedIAMService.CreateAccountCalls())
|
||||
func (mock *IAMServiceMock) CreateAccountCalls() []struct {
|
||||
Access string
|
||||
Account auth.Account
|
||||
} {
|
||||
var calls []struct {
|
||||
Access string
|
||||
Account auth.Account
|
||||
}
|
||||
mock.lockCreateAccount.RLock()
|
||||
calls = mock.calls.CreateAccount
|
||||
mock.lockCreateAccount.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// DeleteUserAccount calls DeleteUserAccountFunc.
|
||||
func (mock *IAMServiceMock) DeleteUserAccount(access string) error {
|
||||
if mock.DeleteUserAccountFunc == nil {
|
||||
panic("IAMServiceMock.DeleteUserAccountFunc: method is nil but IAMService.DeleteUserAccount was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
Access string
|
||||
}{
|
||||
Access: access,
|
||||
}
|
||||
mock.lockDeleteUserAccount.Lock()
|
||||
mock.calls.DeleteUserAccount = append(mock.calls.DeleteUserAccount, callInfo)
|
||||
mock.lockDeleteUserAccount.Unlock()
|
||||
return mock.DeleteUserAccountFunc(access)
|
||||
}
|
||||
|
||||
// DeleteUserAccountCalls gets all the calls that were made to DeleteUserAccount.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedIAMService.DeleteUserAccountCalls())
|
||||
func (mock *IAMServiceMock) DeleteUserAccountCalls() []struct {
|
||||
Access string
|
||||
} {
|
||||
var calls []struct {
|
||||
Access string
|
||||
}
|
||||
mock.lockDeleteUserAccount.RLock()
|
||||
calls = mock.calls.DeleteUserAccount
|
||||
mock.lockDeleteUserAccount.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// GetUserAccount calls GetUserAccountFunc.
|
||||
func (mock *IAMServiceMock) GetUserAccount(access string) (auth.Account, error) {
|
||||
if mock.GetUserAccountFunc == nil {
|
||||
panic("IAMServiceMock.GetUserAccountFunc: method is nil but IAMService.GetUserAccount was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
Access string
|
||||
}{
|
||||
Access: access,
|
||||
}
|
||||
mock.lockGetUserAccount.Lock()
|
||||
mock.calls.GetUserAccount = append(mock.calls.GetUserAccount, callInfo)
|
||||
mock.lockGetUserAccount.Unlock()
|
||||
return mock.GetUserAccountFunc(access)
|
||||
}
|
||||
|
||||
// GetUserAccountCalls gets all the calls that were made to GetUserAccount.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedIAMService.GetUserAccountCalls())
|
||||
func (mock *IAMServiceMock) GetUserAccountCalls() []struct {
|
||||
Access string
|
||||
} {
|
||||
var calls []struct {
|
||||
Access string
|
||||
}
|
||||
mock.lockGetUserAccount.RLock()
|
||||
calls = mock.calls.GetUserAccount
|
||||
mock.lockGetUserAccount.RUnlock()
|
||||
return calls
|
||||
}
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"github.com/versity/versitygw/s3api/controllers"
|
||||
"github.com/versity/versitygw/s3api/utils"
|
||||
"github.com/versity/versitygw/s3err"
|
||||
"github.com/versity/versitygw/s3log"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -40,74 +41,86 @@ type RootUserConfig struct {
|
||||
Secret string
|
||||
}
|
||||
|
||||
func VerifyV4Signature(root RootUserConfig, iam auth.IAMService, region string, debug bool) fiber.Handler {
|
||||
func VerifyV4Signature(root RootUserConfig, iam auth.IAMService, logger s3log.Logger, region string, debug bool) fiber.Handler {
|
||||
acct := accounts{root: root, iam: iam}
|
||||
|
||||
return func(ctx *fiber.Ctx) error {
|
||||
authorization := ctx.Get("Authorization")
|
||||
if authorization == "" {
|
||||
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrAuthHeaderEmpty))
|
||||
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrAuthHeaderEmpty), controllers.LogOptions{Logger: logger, LogType: "auth"})
|
||||
}
|
||||
|
||||
// Check the signature version
|
||||
authParts := strings.Split(authorization, " ")
|
||||
if len(authParts) < 4 {
|
||||
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrMissingFields))
|
||||
}
|
||||
if authParts[0] != "AWS4-HMAC-SHA256" {
|
||||
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrSignatureVersionNotSupported))
|
||||
authParts := strings.Split(authorization, ",")
|
||||
for i, el := range authParts {
|
||||
authParts[i] = strings.TrimSpace(el)
|
||||
}
|
||||
|
||||
credKv := strings.Split(authParts[1], "=")
|
||||
if len(authParts) != 3 {
|
||||
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrMissingFields), controllers.LogOptions{Logger: logger, LogType: "auth"})
|
||||
}
|
||||
|
||||
startParts := strings.Split(authParts[0], " ")
|
||||
|
||||
if startParts[0] != "AWS4-HMAC-SHA256" {
|
||||
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrSignatureVersionNotSupported), controllers.LogOptions{Logger: logger, LogType: "auth"})
|
||||
}
|
||||
|
||||
credKv := strings.Split(startParts[1], "=")
|
||||
if len(credKv) != 2 {
|
||||
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrCredMalformed))
|
||||
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrCredMalformed), controllers.LogOptions{Logger: logger, LogType: "auth"})
|
||||
}
|
||||
creds := strings.Split(credKv[1], "/")
|
||||
if len(creds) < 4 {
|
||||
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrCredMalformed))
|
||||
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrCredMalformed), controllers.LogOptions{Logger: logger, LogType: "auth"})
|
||||
}
|
||||
|
||||
signHdrKv := strings.Split(authParts[2][:len(authParts[2])-1], "=")
|
||||
ctx.Locals("access", creds[0])
|
||||
|
||||
signHdrKv := strings.Split(authParts[1], "=")
|
||||
if len(signHdrKv) != 2 {
|
||||
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrCredMalformed))
|
||||
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrCredMalformed), controllers.LogOptions{Logger: logger, LogType: "auth"})
|
||||
}
|
||||
signedHdrs := strings.Split(signHdrKv[1], ";")
|
||||
|
||||
account, err := acct.getAccount(creds[0])
|
||||
if err == auth.ErrNoSuchUser {
|
||||
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidAccessKeyID))
|
||||
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidAccessKeyID), controllers.LogOptions{Logger: logger, LogType: "auth"})
|
||||
}
|
||||
if err != nil {
|
||||
return controllers.SendResponse(ctx, err)
|
||||
return controllers.SendResponse(ctx, err, controllers.LogOptions{Logger: logger, LogType: "auth"})
|
||||
}
|
||||
|
||||
// Check X-Amz-Date header
|
||||
date := ctx.Get("X-Amz-Date")
|
||||
if date == "" {
|
||||
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrMissingDateHeader))
|
||||
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrMissingDateHeader), controllers.LogOptions{Logger: logger, LogType: "auth"})
|
||||
}
|
||||
|
||||
// Parse the date and check the date validity
|
||||
tdate, err := time.Parse(iso8601Format, date)
|
||||
if err != nil {
|
||||
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrMalformedDate))
|
||||
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrMalformedDate), controllers.LogOptions{Logger: logger, LogType: "auth"})
|
||||
}
|
||||
|
||||
// Calculate the hash of the request payload
|
||||
hashedPayload := sha256.Sum256(ctx.Body())
|
||||
hexPayload := hex.EncodeToString(hashedPayload[:])
|
||||
|
||||
hashPayloadHeader := ctx.Get("X-Amz-Content-Sha256")
|
||||
ok := isSpecialPayload(hashPayloadHeader)
|
||||
|
||||
// Compare the calculated hash with the hash provided
|
||||
if hashPayloadHeader != hexPayload {
|
||||
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrContentSHA256Mismatch))
|
||||
if !ok {
|
||||
// Calculate the hash of the request payload
|
||||
hashedPayload := sha256.Sum256(ctx.Body())
|
||||
hexPayload := hex.EncodeToString(hashedPayload[:])
|
||||
|
||||
// Compare the calculated hash with the hash provided
|
||||
if hashPayloadHeader != hexPayload {
|
||||
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrContentSHA256Mismatch), controllers.LogOptions{Logger: logger, LogType: "auth"})
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new http request instance from fasthttp request
|
||||
req, err := utils.CreateHttpRequestFromCtx(ctx, signedHdrs)
|
||||
if err != nil {
|
||||
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrInternalError))
|
||||
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrInternalError), controllers.LogOptions{Logger: logger, LogType: "auth"})
|
||||
}
|
||||
|
||||
signer := v4.NewSigner()
|
||||
@@ -115,31 +128,34 @@ func VerifyV4Signature(root RootUserConfig, iam auth.IAMService, region string,
|
||||
signErr := signer.SignHTTP(req.Context(), aws.Credentials{
|
||||
AccessKeyID: creds[0],
|
||||
SecretAccessKey: account.Secret,
|
||||
}, req, hexPayload, creds[3], region, tdate, func(options *v4.SignerOptions) {
|
||||
}, req, hashPayloadHeader, creds[3], region, tdate, func(options *v4.SignerOptions) {
|
||||
if debug {
|
||||
options.LogSigning = true
|
||||
options.Logger = logging.NewStandardLogger(os.Stderr)
|
||||
}
|
||||
})
|
||||
if signErr != nil {
|
||||
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrInternalError))
|
||||
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrInternalError), controllers.LogOptions{Logger: logger, LogType: "auth"})
|
||||
}
|
||||
|
||||
parts := strings.Split(req.Header.Get("Authorization"), " ")
|
||||
if len(parts) < 4 {
|
||||
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrMissingFields))
|
||||
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrMissingFields), controllers.LogOptions{Logger: logger, LogType: "auth"})
|
||||
}
|
||||
calculatedSign := strings.Split(parts[3], "=")[1]
|
||||
expectedSign := strings.Split(authParts[3], "=")[1]
|
||||
expectedSign := strings.Split(authParts[2], "=")[1]
|
||||
|
||||
if expectedSign != calculatedSign {
|
||||
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrSignatureDoesNotMatch))
|
||||
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrSignatureDoesNotMatch), controllers.LogOptions{Logger: logger, LogType: "auth"})
|
||||
}
|
||||
|
||||
ctx.Locals("role", account.Role)
|
||||
ctx.Locals("access", creds[0])
|
||||
ctx.Locals("isRoot", creds[0] == root.Access)
|
||||
|
||||
if logger != nil {
|
||||
logger.SendAuthLog(&creds[0], nil)
|
||||
}
|
||||
|
||||
return ctx.Next()
|
||||
}
|
||||
}
|
||||
@@ -159,3 +175,16 @@ func (a accounts) getAccount(access string) (auth.Account, error) {
|
||||
|
||||
return a.iam.GetUserAccount(access)
|
||||
}
|
||||
|
||||
func isSpecialPayload(str string) bool {
|
||||
specialValues := map[string]bool{
|
||||
"UNSIGNED-PAYLOAD": true,
|
||||
"STREAMING-UNSIGNED-PAYLOAD-TRAILER": true,
|
||||
"STREAMING-AWS4-HMAC-SHA256-PAYLOAD": true,
|
||||
"STREAMING-AWS4-HMAC-SHA256-PAYLOAD-TRAILER": true,
|
||||
"STREAMING-AWS4-ECDSA-P256-SHA256-PAYLOAD": true,
|
||||
"STREAMING-AWS4-ECDSA-P256-SHA256-PAYLOAD-TRAILER": true,
|
||||
}
|
||||
|
||||
return specialValues[str]
|
||||
}
|
||||
|
||||
@@ -21,9 +21,10 @@ import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/versity/versitygw/s3api/controllers"
|
||||
"github.com/versity/versitygw/s3err"
|
||||
"github.com/versity/versitygw/s3log"
|
||||
)
|
||||
|
||||
func VerifyMD5Body() fiber.Handler {
|
||||
func VerifyMD5Body(logger s3log.Logger) fiber.Handler {
|
||||
return func(ctx *fiber.Ctx) error {
|
||||
incomingSum := ctx.Get("Content-Md5")
|
||||
if incomingSum == "" {
|
||||
@@ -34,10 +35,9 @@ func VerifyMD5Body() fiber.Handler {
|
||||
calculatedSum := base64.StdEncoding.EncodeToString(sum[:])
|
||||
|
||||
if incomingSum != calculatedSum {
|
||||
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidDigest))
|
||||
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidDigest), controllers.LogOptions{Logger: logger})
|
||||
}
|
||||
|
||||
return ctx.Next()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,19 +19,19 @@ import (
|
||||
"github.com/versity/versitygw/auth"
|
||||
"github.com/versity/versitygw/backend"
|
||||
"github.com/versity/versitygw/s3api/controllers"
|
||||
"github.com/versity/versitygw/s3log"
|
||||
)
|
||||
|
||||
type S3ApiRouter struct{}
|
||||
|
||||
func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMService) {
|
||||
s3ApiController := controllers.New(be)
|
||||
func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMService, logger s3log.Logger) {
|
||||
s3ApiController := controllers.New(be, iam, logger)
|
||||
adminController := controllers.AdminController{IAMService: iam}
|
||||
|
||||
// TODO: think of better routing system
|
||||
app.Post("/create-user", adminController.CreateUser)
|
||||
app.Patch("/create-user", adminController.CreateUser)
|
||||
|
||||
// Admin Delete api
|
||||
app.Delete("/delete-user", adminController.DeleteUser)
|
||||
app.Patch("/delete-user", adminController.DeleteUser)
|
||||
// ListBuckets action
|
||||
app.Get("/", s3ApiController.ListBuckets)
|
||||
|
||||
|
||||
@@ -20,13 +20,15 @@ import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/versity/versitygw/auth"
|
||||
"github.com/versity/versitygw/backend"
|
||||
"github.com/versity/versitygw/s3log"
|
||||
)
|
||||
|
||||
func TestS3ApiRouter_Init(t *testing.T) {
|
||||
type args struct {
|
||||
app *fiber.App
|
||||
be backend.Backend
|
||||
iam auth.IAMService
|
||||
app *fiber.App
|
||||
be backend.Backend
|
||||
iam auth.IAMService
|
||||
logger s3log.Logger
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -45,7 +47,7 @@ func TestS3ApiRouter_Init(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.sa.Init(tt.args.app, tt.args.be, tt.args.iam)
|
||||
tt.sa.Init(tt.args.app, tt.args.be, tt.args.iam, tt.args.logger)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/versity/versitygw/auth"
|
||||
"github.com/versity/versitygw/backend"
|
||||
"github.com/versity/versitygw/s3api/middlewares"
|
||||
"github.com/versity/versitygw/s3log"
|
||||
)
|
||||
|
||||
type S3ApiServer struct {
|
||||
@@ -33,7 +34,7 @@ type S3ApiServer struct {
|
||||
debug bool
|
||||
}
|
||||
|
||||
func New(app *fiber.App, be backend.Backend, root middlewares.RootUserConfig, port, region string, iam auth.IAMService, opts ...Option) (*S3ApiServer, error) {
|
||||
func New(app *fiber.App, be backend.Backend, root middlewares.RootUserConfig, port, region string, iam auth.IAMService, l s3log.Logger, opts ...Option) (*S3ApiServer, error) {
|
||||
server := &S3ApiServer{
|
||||
app: app,
|
||||
backend: be,
|
||||
@@ -50,10 +51,10 @@ func New(app *fiber.App, be backend.Backend, root middlewares.RootUserConfig, po
|
||||
app.Use(middlewares.RequestLogger(server.debug))
|
||||
|
||||
// Authentication middlewares
|
||||
app.Use(middlewares.VerifyV4Signature(root, iam, region, server.debug))
|
||||
app.Use(middlewares.VerifyMD5Body())
|
||||
app.Use(middlewares.VerifyV4Signature(root, iam, l, region, server.debug))
|
||||
app.Use(middlewares.VerifyMD5Body(l))
|
||||
|
||||
server.router.Init(app, be, iam)
|
||||
server.router.Init(app, be, iam, l)
|
||||
|
||||
return server, nil
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
package s3api
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
@@ -63,7 +64,7 @@ func TestNew(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotS3ApiServer, err := New(tt.args.app, tt.args.be, tt.args.root,
|
||||
tt.args.port, "us-east-1", &auth.IAMServiceInternal{})
|
||||
tt.args.port, "us-east-1", &auth.IAMServiceInternal{}, nil)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
@@ -82,15 +83,26 @@ func TestS3ApiServer_Serve(t *testing.T) {
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Return error when serving S3 api server with invalid address",
|
||||
name: "Serve-invalid-address",
|
||||
wantErr: true,
|
||||
sa: &S3ApiServer{
|
||||
app: fiber.New(),
|
||||
backend: backend.BackendUnsupported{},
|
||||
port: "Wrong address",
|
||||
port: "Invalid address",
|
||||
router: &S3ApiRouter{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Serve-invalid-address-with-certificate",
|
||||
wantErr: true,
|
||||
sa: &S3ApiServer{
|
||||
app: fiber.New(),
|
||||
backend: backend.BackendUnsupported{},
|
||||
port: "Invalid address",
|
||||
router: &S3ApiRouter{},
|
||||
cert: &tls.Certificate{},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
@@ -117,3 +117,39 @@ func TestGetUserMetaData(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_includeHeader(t *testing.T) {
|
||||
type args struct {
|
||||
hdr string
|
||||
signedHdrs []string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "include-header-falsy-case",
|
||||
args: args{
|
||||
hdr: "Content-Type",
|
||||
signedHdrs: []string{"X-Amz-Acl", "Content-Encoding"},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "include-header-falsy-case",
|
||||
args: args{
|
||||
hdr: "Content-Type",
|
||||
signedHdrs: []string{"X-Amz-Acl", "Content-Type"},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := includeHeader(tt.args.hdr, tt.args.signedHdrs); got != tt.want {
|
||||
t.Errorf("includeHeader() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
165
s3log/kafka.go
Normal file
165
s3log/kafka.go
Normal file
@@ -0,0 +1,165 @@
|
||||
// Copyright 2023 Versity Software
|
||||
// This file is licensed under the Apache License, Version 2.0
|
||||
// (the "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package s3log
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Shopify/sarama"
|
||||
"github.com/versity/versitygw/s3err"
|
||||
)
|
||||
|
||||
type KafkaLogger struct {
|
||||
StorageSystem string
|
||||
Action string
|
||||
UserAccess *string
|
||||
Bucket *string
|
||||
Object *string
|
||||
Time time.Time
|
||||
Response any
|
||||
Error *LogError
|
||||
topic string
|
||||
key string
|
||||
producer sarama.SyncProducer
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func InitKafkaLogger(storageSystem, url, topic, key string) (Logger, error) {
|
||||
if topic == "" {
|
||||
return nil, fmt.Errorf("kafka message topic should be specified")
|
||||
}
|
||||
|
||||
config := sarama.NewConfig()
|
||||
config.Producer.Return.Successes = true
|
||||
|
||||
producer, err := sarama.NewSyncProducer([]string{url}, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &KafkaLogger{
|
||||
StorageSystem: storageSystem,
|
||||
topic: topic,
|
||||
key: key,
|
||||
producer: producer,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (l *KafkaLogger) SendSuccessLog(data any, action string, access, bucket, object *string) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
l.Action = action
|
||||
l.UserAccess = access
|
||||
l.Bucket = bucket
|
||||
l.Object = object
|
||||
l.Response = data
|
||||
l.Time = time.Now()
|
||||
l.Error = nil
|
||||
|
||||
l.sendLog(nil)
|
||||
}
|
||||
|
||||
func (l *KafkaLogger) SendErrorLog(err error, action string, access, bucket, object *string) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
l.Action = action
|
||||
l.UserAccess = access
|
||||
l.Bucket = bucket
|
||||
l.Object = object
|
||||
serr, ok := err.(s3err.APIError)
|
||||
if ok {
|
||||
l.Error = &LogError{
|
||||
StatusCode: serr.HTTPStatusCode,
|
||||
Message: serr.Description,
|
||||
}
|
||||
} else {
|
||||
l.Error = &LogError{
|
||||
StatusCode: 500,
|
||||
Message: err.Error(),
|
||||
}
|
||||
}
|
||||
l.Response = nil
|
||||
|
||||
l.sendLog(nil)
|
||||
}
|
||||
|
||||
func (l *KafkaLogger) SendAuthLog(access *string, err error) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
if err != nil {
|
||||
serr, ok := err.(s3err.APIError)
|
||||
if ok {
|
||||
l.sendLog(AuthErrorLog{
|
||||
StorageSystem: l.StorageSystem,
|
||||
Time: time.Now(),
|
||||
UserAccess: access,
|
||||
ErrorMessage: serr.Description,
|
||||
ErrorStatus: serr.HTTPStatusCode,
|
||||
ErrorType: "Authentication error",
|
||||
})
|
||||
} else {
|
||||
l.sendLog(AuthErrorLog{
|
||||
StorageSystem: l.StorageSystem,
|
||||
Time: time.Now(),
|
||||
UserAccess: access,
|
||||
ErrorMessage: err.Error(),
|
||||
ErrorStatus: 500,
|
||||
ErrorType: "Authentication error",
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
l.sendLog(AuthSuccessLog{
|
||||
StorageSystem: l.StorageSystem,
|
||||
Time: time.Now(),
|
||||
UserAccess: access,
|
||||
Message: "The user passed the authentication successfully",
|
||||
})
|
||||
}
|
||||
|
||||
func (l *KafkaLogger) sendLog(data any) {
|
||||
if data == nil {
|
||||
data = l
|
||||
}
|
||||
msg, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
fmt.Printf("\n failed to parse the log data: %v", err.Error())
|
||||
}
|
||||
|
||||
var message *sarama.ProducerMessage
|
||||
|
||||
if l.key == "" {
|
||||
message = &sarama.ProducerMessage{
|
||||
Topic: l.topic,
|
||||
Value: sarama.StringEncoder(msg),
|
||||
}
|
||||
} else {
|
||||
message = &sarama.ProducerMessage{
|
||||
Topic: l.topic,
|
||||
Key: sarama.StringEncoder(l.key),
|
||||
Value: sarama.StringEncoder(msg),
|
||||
}
|
||||
}
|
||||
|
||||
_, _, err = l.producer.SendMessage(message)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
||||
69
s3log/logger.go
Normal file
69
s3log/logger.go
Normal file
@@ -0,0 +1,69 @@
|
||||
// Copyright 2023 Versity Software
|
||||
// This file is licensed under the Apache License, Version 2.0
|
||||
// (the "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package s3log
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
type LoggerType string
|
||||
|
||||
const (
|
||||
WebhookLoggerType LoggerType = "webhook"
|
||||
)
|
||||
|
||||
type Logger interface {
|
||||
SendErrorLog(err error, action string, access, bucket, object *string)
|
||||
SendSuccessLog(data any, action string, access, bucket, object *string)
|
||||
SendAuthLog(access *string, err error)
|
||||
}
|
||||
|
||||
type AuthSuccessLog struct {
|
||||
StorageSystem string
|
||||
Time time.Time
|
||||
UserAccess *string
|
||||
Message string
|
||||
}
|
||||
|
||||
type AuthErrorLog struct {
|
||||
StorageSystem string
|
||||
Time time.Time
|
||||
UserAccess *string
|
||||
ErrorMessage string
|
||||
ErrorStatus int
|
||||
ErrorType string
|
||||
}
|
||||
|
||||
type LogConfig struct {
|
||||
WebhookURL string
|
||||
KafkaURL string
|
||||
KafkaTopic string
|
||||
KafkaTopicKey string
|
||||
StorageSystem string
|
||||
}
|
||||
|
||||
func InitLogger(cfg *LogConfig) (Logger, error) {
|
||||
if cfg.WebhookURL != "" && cfg.KafkaURL != "" {
|
||||
return nil, fmt.Errorf("specify one of 2 option for audit logging: kafka, webhook")
|
||||
}
|
||||
if cfg.WebhookURL != "" {
|
||||
return InitWebhookLogger(cfg.StorageSystem, cfg.WebhookURL)
|
||||
}
|
||||
if cfg.KafkaURL != "" {
|
||||
return InitKafkaLogger(cfg.StorageSystem, cfg.KafkaURL, cfg.KafkaTopic, cfg.KafkaTopicKey)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
161
s3log/webhook.go
Normal file
161
s3log/webhook.go
Normal file
@@ -0,0 +1,161 @@
|
||||
// Copyright 2023 Versity Software
|
||||
// This file is licensed under the Apache License, Version 2.0
|
||||
// (the "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package s3log
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/versity/versitygw/s3err"
|
||||
)
|
||||
|
||||
type LogError struct {
|
||||
StatusCode int
|
||||
Message string
|
||||
}
|
||||
|
||||
type WebhookLogger struct {
|
||||
StorageSystem string
|
||||
Time time.Time
|
||||
Action string
|
||||
UserAccess *string
|
||||
Bucket *string
|
||||
Object *string
|
||||
Response any
|
||||
Error *LogError
|
||||
url string
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
var _ Logger = &WebhookLogger{}
|
||||
|
||||
func InitWebhookLogger(storageSystem, url string) (Logger, error) {
|
||||
client := &http.Client{
|
||||
Timeout: 3 * time.Second,
|
||||
}
|
||||
_, err := client.Post(url, "application/json", nil)
|
||||
if err != nil {
|
||||
if err, ok := err.(net.Error); ok && !err.Timeout() {
|
||||
return nil, fmt.Errorf("unreachable webhook url")
|
||||
}
|
||||
}
|
||||
return &WebhookLogger{
|
||||
url: url,
|
||||
StorageSystem: storageSystem,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (l *WebhookLogger) SendSuccessLog(data any, action string, access, bucket, object *string) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
l.Action = action
|
||||
l.UserAccess = access
|
||||
l.Bucket = bucket
|
||||
l.Object = object
|
||||
l.Response = data
|
||||
l.Time = time.Now()
|
||||
l.Error = nil
|
||||
|
||||
l.sendLog(nil)
|
||||
}
|
||||
|
||||
func (l *WebhookLogger) SendErrorLog(err error, action string, access, bucket, object *string) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
l.Action = action
|
||||
l.UserAccess = access
|
||||
l.Bucket = bucket
|
||||
l.Object = object
|
||||
serr, ok := err.(s3err.APIError)
|
||||
if ok {
|
||||
l.Error = &LogError{
|
||||
StatusCode: serr.HTTPStatusCode,
|
||||
Message: serr.Description,
|
||||
}
|
||||
} else {
|
||||
l.Error = &LogError{
|
||||
StatusCode: 500,
|
||||
Message: err.Error(),
|
||||
}
|
||||
}
|
||||
l.Response = nil
|
||||
|
||||
l.sendLog(nil)
|
||||
}
|
||||
|
||||
func (l *WebhookLogger) SendAuthLog(access *string, err error) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
if err != nil {
|
||||
serr, ok := err.(s3err.APIError)
|
||||
if ok {
|
||||
l.sendLog(AuthErrorLog{
|
||||
StorageSystem: l.StorageSystem,
|
||||
Time: time.Now(),
|
||||
UserAccess: access,
|
||||
ErrorMessage: serr.Description,
|
||||
ErrorStatus: serr.HTTPStatusCode,
|
||||
ErrorType: "Authentication error",
|
||||
})
|
||||
} else {
|
||||
l.sendLog(AuthErrorLog{
|
||||
StorageSystem: l.StorageSystem,
|
||||
Time: time.Now(),
|
||||
UserAccess: access,
|
||||
ErrorMessage: err.Error(),
|
||||
ErrorStatus: 500,
|
||||
ErrorType: "Authentication error",
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
l.sendLog(AuthSuccessLog{
|
||||
StorageSystem: l.StorageSystem,
|
||||
Time: time.Now(),
|
||||
UserAccess: access,
|
||||
Message: "The user passed the authentication successfully",
|
||||
})
|
||||
}
|
||||
|
||||
func (l *WebhookLogger) sendLog(data any) {
|
||||
if data == nil {
|
||||
data = l
|
||||
}
|
||||
jsonLog, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
fmt.Printf("\n failed to parse the log data: %v", err.Error())
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, l.url, bytes.NewReader(jsonLog))
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json; charset=utf-8")
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 3 * time.Second,
|
||||
}
|
||||
_, err = client.Do(req)
|
||||
if err != nil {
|
||||
fmt.Printf("\n failed to send the log %v", err.Error())
|
||||
}
|
||||
}
|
||||
@@ -94,3 +94,16 @@ type Owner struct {
|
||||
ID string
|
||||
DisplayName string
|
||||
}
|
||||
|
||||
type Tag struct {
|
||||
Key string `xml:"Key"`
|
||||
Value string `xml:"Value"`
|
||||
}
|
||||
|
||||
type TagSet struct {
|
||||
Tags []Tag `xml:"Tag"`
|
||||
}
|
||||
|
||||
type Tagging struct {
|
||||
TagSet TagSet `xml:"TagSet"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user