Compare commits

...

148 Commits
v0.4 ... v0.7

Author SHA1 Message Date
Ben McClelland
e46e4e941b Merge pull request #255 from versity/fix/issue-245-upload_part_copy-invalid-range
Issue 245, UploadPartCopy exceeding range error
2023-09-19 09:04:05 -07:00
jonaustin09
c9653cff71 fix: Fixes #245, Fixed exceeding range error for UploadPartCopy action 2023-09-19 11:53:49 -04:00
Ben McClelland
48798c9e39 Merge pull request #254 from versity/dependabot/go_modules/dev-dependencies-e5940eaa8e
chore(deps): bump the dev-dependencies group with 4 updates
2023-09-18 17:21:42 -07:00
dependabot[bot]
42c4ad3b9e chore(deps): bump the dev-dependencies group with 4 updates
Bumps the dev-dependencies group with 4 updates: [github.com/nats-io/nats.go](https://github.com/nats-io/nats.go), [github.com/valyala/fasthttp](https://github.com/valyala/fasthttp), [github.com/aws/aws-sdk-go-v2/config](https://github.com/aws/aws-sdk-go-v2) and [github.com/aws/aws-sdk-go-v2/feature/s3/manager](https://github.com/aws/aws-sdk-go-v2).


Updates `github.com/nats-io/nats.go` from 1.28.0 to 1.29.0
- [Release notes](https://github.com/nats-io/nats.go/releases)
- [Commits](https://github.com/nats-io/nats.go/compare/v1.28.0...v1.29.0)

Updates `github.com/valyala/fasthttp` from 1.49.0 to 1.50.0
- [Release notes](https://github.com/valyala/fasthttp/releases)
- [Commits](https://github.com/valyala/fasthttp/compare/v1.49.0...v1.50.0)

Updates `github.com/aws/aws-sdk-go-v2/config` from 1.18.39 to 1.18.40
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/config/v1.18.39...config/v1.18.40)

Updates `github.com/aws/aws-sdk-go-v2/feature/s3/manager` from 1.11.83 to 1.11.84
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/feature/s3/manager/v1.11.83...feature/s3/manager/v1.11.84)

---
updated-dependencies:
- dependency-name: github.com/nats-io/nats.go
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: github.com/valyala/fasthttp
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: github.com/aws/aws-sdk-go-v2/config
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: github.com/aws/aws-sdk-go-v2/feature/s3/manager
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-18 21:59:05 +00:00
Jon Austin
1874d3c329 fix: Fixes #244, Added destincation bucket ACL check for CopyObject action (#253) 2023-09-18 11:49:37 -07:00
Ben McClelland
9f0c9badba Merge pull request #252 from versity/fix/issue-243-copy-object-same-dest
Issue 243 CopyObject error case to copy the object into itself
2023-09-18 09:34:04 -07:00
jonaustin09
cb6b60324c fix: Fixes #243, fixed the error case for CopyObject to copy the object into itself 2023-09-18 09:29:25 -07:00
Ben McClelland
c53707d4ae Merge pull request #251 from versity/fix/walk-last-elem
Walk function last object check panic fix
2023-09-18 08:29:08 -07:00
jonaustin09
ee1ab5bdcc fix: Changed Walk function last object check to avoid panic 2023-09-18 08:20:08 -04:00
Ben McClelland
7a0c4423e4 Merge pull request #242 from versity/dependabot/go_modules/github.com/gofiber/fiber/v2-2.49.2
chore(deps): bump github.com/gofiber/fiber/v2 from 2.49.1 to 2.49.2
2023-09-14 18:18:57 -07:00
Ben McClelland
8382911ab6 Merge pull request #241 from versity/fix/list-objects-bugs
Issue 179, 180
2023-09-14 18:18:18 -07:00
dependabot[bot]
4e7615b4fd chore(deps): bump github.com/gofiber/fiber/v2 from 2.49.1 to 2.49.2
Bumps [github.com/gofiber/fiber/v2](https://github.com/gofiber/fiber) from 2.49.1 to 2.49.2.
- [Release notes](https://github.com/gofiber/fiber/releases)
- [Commits](https://github.com/gofiber/fiber/compare/v2.49.1...v2.49.2)

---
updated-dependencies:
- dependency-name: github.com/gofiber/fiber/v2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-14 20:19:54 +00:00
jonaustin09
8951cce6d0 fix: Fixes #179, Fixes #180, Fixes ListObject marker bug 2023-09-14 16:17:29 -04:00
Ben McClelland
363c82971a Merge pull request #240 from versity/fix/issue-239
Issue 239, authentication time comparison with UTC
2023-09-13 12:35:07 -07:00
jonaustin09
cf1c44969b fix: Fixes #239, Change SigV4 date comparison with UTC 2023-09-13 15:16:50 -04:00
Ben McClelland
37b5429468 Merge pull request #235 from versity/ben/admin_server
fix: remove body limit for admin app
2023-09-13 07:16:50 -07:00
Ben McClelland
c9475adb04 fix: remove body limit for admin app
The BodyLimit is needed for large PUTs in the s3 api server, but
the admin server requests are not expected to exceed the default
4MB limit.
2023-09-12 16:16:47 -07:00
Ben McClelland
b00819ff31 Merge pull request #234 from versity/feat/admin-server
Issue 232, An option to run admin server as a separate one.
2023-09-12 16:12:20 -07:00
jonaustin09
5ab38e3dab feat: Closes #232, Added an option to run admin server in a different network, by specifying admin server address/ip 2023-09-12 16:04:31 -07:00
Ben McClelland
c04f6d7f00 Merge pull request #233 from versity/admin/list-buckets
Issue 217, Admin API and CLI action to list all the buckets and its owners.
2023-09-12 16:02:29 -07:00
jonaustin09
6ac69b3198 feat: Closes #217, Created an admin API and CLI action to list all the buckets and its owners as a table 2023-09-12 08:29:34 -04:00
Ben McClelland
c72686f7fa Merge pull request #223 from versity/ben/32bit
fix: builds for non 64 bit linux arch
2023-09-11 09:10:30 -07:00
Ben McClelland
145c2dd4e3 fix: builds for non 64 bit linux arch
The scoutfs backend is only supported on 64bit linux.  This corrects
the build constraints to only supported linux arch, and prevents the
incompatible import for unspported arch.

We also need to adjust the body limit setting on 32 bit since this
is an int, and our default limit will overfow on 32 bit.
2023-09-10 20:49:56 -07:00
Ben McClelland
f90562fea2 Merge pull request #231 from versity/dependabot/go_modules/dev-dependencies-8b25ff15ea
chore(deps): bump the dev-dependencies group with 1 update
2023-09-10 20:36:15 -07:00
dependabot[bot]
b0b22467cc chore(deps): bump the dev-dependencies group with 1 update
Bumps the dev-dependencies group with 1 update: [github.com/gofiber/fiber/v2](https://github.com/gofiber/fiber).

- [Release notes](https://github.com/gofiber/fiber/releases)
- [Commits](https://github.com/gofiber/fiber/compare/v2.48.0...v2.49.1)

---
updated-dependencies:
- dependency-name: github.com/gofiber/fiber/v2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-10 19:21:20 +00:00
Ben McClelland
b2247e20ee Merge pull request #230 from versity/ben/dependabot
chore: add grouping for dependabot PRs
2023-09-10 12:20:47 -07:00
Ben McClelland
8017b0cff0 Merge pull request #229 from versity/dependabot/go_modules/golang.org/x/sys-0.12.0
chore(deps): bump golang.org/x/sys from 0.10.0 to 0.12.0
2023-09-10 12:20:31 -07:00
dependabot[bot]
c1b105d928 chore(deps): bump golang.org/x/sys from 0.10.0 to 0.12.0
Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.10.0 to 0.12.0.
- [Commits](https://github.com/golang/sys/compare/v0.10.0...v0.12.0)

---
updated-dependencies:
- dependency-name: golang.org/x/sys
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-10 19:15:49 +00:00
Ben McClelland
57c7518864 Merge pull request #228 from versity/dependabot/go_modules/github.com/aws/aws-sdk-go-v2/feature/s3/manager-1.11.83
chore(deps): bump github.com/aws/aws-sdk-go-v2/feature/s3/manager from 1.11.76 to 1.11.83
2023-09-10 12:15:07 -07:00
dependabot[bot]
5a94a70212 chore(deps): bump github.com/aws/aws-sdk-go-v2/feature/s3/manager
Bumps [github.com/aws/aws-sdk-go-v2/feature/s3/manager](https://github.com/aws/aws-sdk-go-v2) from 1.11.76 to 1.11.83.
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/feature/s3/manager/v1.11.76...feature/s3/manager/v1.11.83)

---
updated-dependencies:
- dependency-name: github.com/aws/aws-sdk-go-v2/feature/s3/manager
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-10 19:09:21 +00:00
Ben McClelland
064230108f Merge pull request #227 from versity/dependabot/go_modules/github.com/valyala/fasthttp-1.49.0
chore(deps): bump github.com/valyala/fasthttp from 1.48.0 to 1.49.0
2023-09-10 12:08:23 -07:00
Ben McClelland
51680d445c chore: add grouping for dependabot PRs 2023-09-10 12:07:09 -07:00
dependabot[bot]
732e92a72f chore(deps): bump github.com/valyala/fasthttp from 1.48.0 to 1.49.0
Bumps [github.com/valyala/fasthttp](https://github.com/valyala/fasthttp) from 1.48.0 to 1.49.0.
- [Release notes](https://github.com/valyala/fasthttp/releases)
- [Commits](https://github.com/valyala/fasthttp/compare/v1.48.0...v1.49.0)

---
updated-dependencies:
- dependency-name: github.com/valyala/fasthttp
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-10 18:59:04 +00:00
Ben McClelland
36aea696c6 Merge pull request #226 from versity/dependabot/go_modules/github.com/aws/aws-sdk-go-v2-1.21.0
chore(deps): bump github.com/aws/aws-sdk-go-v2 from 1.20.0 to 1.21.0
2023-09-10 11:58:45 -07:00
Ben McClelland
46c0762133 Merge pull request #225 from versity/dependabot/go_modules/github.com/google/uuid-1.3.1
chore(deps): bump github.com/google/uuid from 1.3.0 to 1.3.1
2023-09-10 11:58:19 -07:00
dependabot[bot]
8d6b5c387f chore(deps): bump github.com/aws/aws-sdk-go-v2 from 1.20.0 to 1.21.0
Bumps [github.com/aws/aws-sdk-go-v2](https://github.com/aws/aws-sdk-go-v2) from 1.20.0 to 1.21.0.
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/v1.20.0...v1.21.0)

---
updated-dependencies:
- dependency-name: github.com/aws/aws-sdk-go-v2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-10 18:52:04 +00:00
dependabot[bot]
a241e6a7e6 chore(deps): bump github.com/google/uuid from 1.3.0 to 1.3.1
Bumps [github.com/google/uuid](https://github.com/google/uuid) from 1.3.0 to 1.3.1.
- [Release notes](https://github.com/google/uuid/releases)
- [Changelog](https://github.com/google/uuid/blob/master/CHANGELOG.md)
- [Commits](https://github.com/google/uuid/compare/v1.3.0...v1.3.1)

---
updated-dependencies:
- dependency-name: github.com/google/uuid
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-10 18:51:27 +00:00
Ben McClelland
c690b01a90 Merge pull request #224 from versity/ben/dependabot
chore: add dependabot.yml configuration
2023-09-10 11:51:03 -07:00
Ben McClelland
0d044c2303 chore: add dependabot.yml configuration 2023-09-10 11:47:39 -07:00
Ben McClelland
42270fbe1c Merge pull request #222 from versity/minor_app_cleanup
App cleanup(minor changes)
2023-09-08 09:51:29 -07:00
jonaustin09
35fe6d8dee fix: some cleanup in posix, router and acl 2023-09-08 12:46:50 -04:00
Ben McClelland
23b5eb30ed Merge pull request #220 from versity/ben/goreleaser
fix: goreleaser remove merge commits from release changelog
2023-09-08 09:36:39 -07:00
Ben McClelland
24309ae25a Merge pull request #221 from versity/int_test_create_bucket
CreateBucket integration test
2023-09-08 09:36:18 -07:00
jonaustin09
f74179d01c feat: Added an integration test case for CreateBucket to create a bucket as user 2023-09-08 12:28:01 -04:00
Ben McClelland
1959fac8a0 fix: goreleaser remove merge commits from release changelog 2023-09-08 09:09:36 -07:00
Ben McClelland
eb05f5a93e Merge pull request #219 from versity/vet-warnings
Issue 206, vet warnings
2023-09-08 09:06:28 -07:00
jonaustin09
23c26d802c fix: Fixes #216, Fixed vet warnings, removed the code snippet which copied fiber.Ctx 2023-09-08 11:57:17 -04:00
Ben McClelland
2ef5578baf Merge pull request #218 from versity/ben/goreleaser
feat: setup goreleaser to manage release artifacts when tagged
2023-09-07 22:23:35 -07:00
Ben McClelland
473ff0f4d5 feat: setup goreleaser to manage release artifacts when tagged 2023-09-07 22:18:16 -07:00
Ben McClelland
08c0118839 Merge pull request #215 from versity/ben/admin_env
fix: add ADMIN_ENDPOINT_URL env var to admin cli
2023-09-07 21:08:13 -07:00
Ben McClelland
6ab4090216 fix: add ADMIN_ENDPOINT_URL env var to admin cli 2023-09-07 13:17:54 -07:00
Ben McClelland
3360466b5e Merge pull request #214 from versity/fix/issue-204-list-buckets
Issue 204, ListBuckets for admin and regular users
2023-09-07 12:04:23 -07:00
jonaustin09
8d2e2a4106 fix: Fixes #204, Change ListBuckets action logic to return all the buckets for admin users and the buckets owned by a user for regular users. Added integration test cases for ListBuckets action 2023-09-07 14:49:47 -04:00
Ben McClelland
d320c953d3 Merge pull request #213 from versity/ben/list_accounts
feat: format admin cli list-users output in table
2023-09-07 09:50:12 -07:00
Ben McClelland
ef92f57e7d feat: format admin cli list-users output in table
Format list-users output in an easier to read table:
% versitygw admin list-users
Account  Role
-------  ----
myadmin  admin
myuser   user
2023-09-07 08:48:18 -07:00
Ben McClelland
17651fc139 Merge pull request #212 from versity/feat/issue-206-bucket-owner-assignment
Issue 206, Change bucket owner admin API and CLI action
2023-09-06 15:20:38 -07:00
jonaustin09
7620651a49 fix: Merge conflicts resolved with main 2023-09-06 17:45:23 -04:00
jonaustin09
4c7584c99f feat: Closes #206, Added an admin api endpoint and a CLI action to change buckets owner 2023-09-06 17:41:47 -04:00
Ben McClelland
fc4780020b Merge pull request #210 from versity/ben/iam_acct
fix: move auth internal UserAcc to auth.Account
2023-09-06 08:29:10 -07:00
Ben McClelland
df81ead6bc fix: move auth internal UserAcc to auth.Account 2023-09-05 16:21:21 -07:00
Ben McClelland
d7148105be Merge pull request #209 from versity/feat/issue-205-list-accs
Issue 205 list users in admin CLI
2023-09-05 15:23:32 -07:00
jonaustin09
2bcfa0e01b Merge branch 'main' of https://github.com/versity/versitygw into feat/issue-205-list-accs 2023-09-05 18:12:23 -04:00
jonaustin09
d80580380d feat: Closes #205, Add admin api endpoint and CLI action to list users. Added unit tests for the api endpoint 2023-09-05 18:12:11 -04:00
Ben McClelland
4d50d970ea Merge pull request #208 from versity/fix/issue-198
Issue-207, 198
2023-09-05 11:07:15 -07:00
jonaustin09
cb2f6a87aa fix: Fixes #207, Fixes #198: added lexicographical order by object key and uploadId for ListMultipartUploads response, Added FE support to pass the necessary arguments to BE for ListMultipartUploads 2023-09-01 15:33:58 -04:00
Ben McClelland
3d129789e0 fix: update README.md minor formatting 2023-08-31 13:44:25 -07:00
Ben McClelland
07e0372531 fix: update README.md with minor changes 2023-08-31 13:43:28 -07:00
Ben McClelland
49e70f9385 Merge pull request #203 from versity/sigV4-errors-refactoring
Authentication errors refactoring
2023-08-31 12:01:13 -07:00
jonaustin09
53cf4f342f feat: Added more integration test cases for the authentication and md5 checker 2023-08-30 23:21:09 +04:00
jonaustin09
a58ce0c238 feat: Added 8 integration test cases for authentication 2023-08-29 23:46:54 +04:00
jonaustin09
3573a31ae6 fix: Closes #192, Fixed authentication errors returned, created integration test cases for it 2023-08-25 21:50:21 +04:00
Ben McClelland
9dafc0e73b Merge pull request #202 from versity/ben/backend_interface
fix: cleanup backend interface functions ordering
2023-08-25 10:04:59 -07:00
Ben McClelland
d058dcb898 fix: cleanup backend interface functions ordering 2023-08-25 09:15:01 -07:00
Ben McClelland
07a8efe4d3 Merge pull request #201 from versity/ben/actions
fix: update github actions order to enable module caching
2023-08-25 09:14:40 -07:00
Ben McClelland
e1f8cbc346 fix: update github actions order to enable module caching 2023-08-24 21:48:43 -07:00
Ben McClelland
05d6e618b2 Merge pull request #200 from versity/fix/issue-197
Issue 197
2023-08-24 13:54:36 -07:00
jonaustin09
e8b06a72f9 fix: Fixes #197, Fixed PutBucketAcl action input validation 2023-08-25 00:40:17 +04:00
Ben McClelland
c389e1b28c Merge pull request #199 from versity/fix/issue-195
Issue 195
2023-08-24 13:31:31 -07:00
jonaustin09
a2439264b2 fix: Fixes #195, fixed DeleteObjects action response structure 2023-08-24 18:37:01 +04:00
Ben McClelland
56f452f1a2 Merge pull request #194 from versity/int-tests-restruct
Integration tests restructuring
2023-08-23 09:40:07 -07:00
jonaustin09
a05179b14f feat: Added integration test cases for PutBucketAcl, GetBucketAcl actions 2023-08-23 17:24:16 +04:00
jonaustin09
22227c875a feat: Added integration test cases for CreateMultipartUpload, UploadPart, UploadPartCopy, ListParts, ListMultipartUpload, CompleteMultipartUpload 2023-08-22 20:20:27 +04:00
Ben McClelland
da99990225 Merge pull request #196 from versity/ben/readme
readme updates to beta and use clarification
2023-08-21 17:21:42 -07:00
Ben McClelland
cb9a7853f9 readme updates to beta and use clarification 2023-08-14 22:24:57 -07:00
jonaustin09
da3ad55483 feat: Added integration test cases for HeadBucket, HeadObject, DeleteObject, DeleteObjects, ListObjects 2023-08-11 23:33:50 +04:00
jonaustin09
2cc0c7203c feat: Closes #189, added utility functions for testing, restructured tests for CreateBucket, DeleteBucket... and 5 more actions 2023-08-11 02:13:01 +04:00
Ben McClelland
a325dd6834 Merge pull request #193 from versity/fix/issue-181
Objects keys with special characters
2023-08-07 15:15:22 -07:00
jonaustin09
7814979efa feat: Fixes #181, Added support to add object with special character keys, disabled URI path escaping in v4 signing, add a middleware to parse the URL and store the decoded version as a new URL, added test cases for adding/getting/listing objects with special characters 2023-08-08 00:41:06 +04:00
Ben McClelland
059507deae Merge pull request #191 from versity/fix/issue-184
Issue 184
2023-08-04 09:32:53 -07:00
jonaustin09
7d8a795e95 fix: Fixes #184, Change InvalidArgument to InvalidRange error for GetObject by range for larger ragnes 2023-08-04 20:27:47 +04:00
Ben McClelland
1d662e93c5 Merge pull request #190 from versity/fix/issue-182
Issue 186
2023-08-03 17:49:20 -07:00
Jon Austin
cc0316aa99 Merge branch 'main' into fix/issue-182 2023-08-03 14:44:48 -07:00
jonaustin09
cc28535618 fix: Fixes #186, Fixed object metadata storing and retrieval flow in PutObject and GetObject actions 2023-08-04 01:43:30 +04:00
Ben McClelland
bc131d5f99 Merge pull request #188 from versity/fix/issue-182
Issue 182
2023-08-03 11:48:13 -07:00
Ben McClelland
13ce76ba21 Merge pull request #187 from versity/fix/issue-183
Issue 183
2023-08-03 11:47:08 -07:00
jonaustin09
67fc857cdd fix: Fixes #182, fixed max-keys 0 case to not return any object key 2023-08-03 22:39:28 +04:00
jonaustin09
dde13ddc9a fix: Fixes #183. Added a validation for max-keys for ListObjects/ListObjectsV2 2023-08-03 20:47:34 +04:00
Ben McClelland
34830954c3 Merge pull request #178 from versity/ben/deps
fix: upgrade module dependencies
2023-08-01 21:54:10 -07:00
Ben McClelland
77a4a9e3a5 fix: upgrade module dependencies 2023-08-01 21:50:17 -07:00
Ben McClelland
25b02dc8fa Merge pull request #177 from versity/select-object-content-fe
SelectObjectContent FE
2023-08-01 13:37:29 -07:00
jonaustin09
009ceee748 feat: Added FE support for SelectObjectContent action 2023-08-02 00:08:28 +04:00
Ben McClelland
af69adf080 Merge pull request #176 from versity/fix/s3response-cleanup
s3response cleanup
2023-07-31 21:45:18 -07:00
jonaustin09
97847735c8 fix: s3response action responses naming cleanup 2023-07-31 21:41:10 -07:00
Ben McClelland
ac9aa25ff1 Merge pull request #175 from versity/fix/issue-143
Issue 143
2023-07-31 21:38:10 -07:00
Jon Austin
091375fa00 Issue 151 (#174)
* fix: Fixes #151. Fixed DeleteObjects action bugs: Corrected request body serialization type, added return type
2023-07-31 21:36:33 -07:00
Ben McClelland
f1e22b0a4d Merge pull request #173 from versity/fix/issue-168
Issu 168
2023-07-31 21:35:00 -07:00
jonaustin09
3f8c218431 fix: Fixes #143. Fixed action name in bucket creation admin checker response handler 2023-07-31 20:54:16 +04:00
jonaustin09
70818de594 fix: Fixes #168. Changed PutObject existing object error from custom internal error to ErrExistingObjectIsDirectory 2023-07-31 18:17:29 +04:00
Ben McClelland
366ed21ede Merge pull request #172 from versity/fix/issue-152
Issue 152
2023-07-28 21:24:39 -07:00
Ben McClelland
b96da570a7 Merge pull request #171 from versity/fix/issue-153
Issue 153
2023-07-28 21:23:22 -07:00
jonaustin09
898c3efaa0 fix: Fixes #153. Fixed CompleteMultipartUpload invalid ETag error case, fixed UploadPart xattr.Set error 2023-07-28 18:20:07 +04:00
jonaustin09
838a7f9ef9 fix: Fixes #152. Changed CompleteMultiPartUpload invalid payload error to MalformedXML 2023-07-28 18:19:15 +04:00
Jon Austin
bf33b9f5a2 Issue 154 (#169)
* fix: Fixes #154, Changed GetObject range error to InvalidRange
2023-07-27 11:05:40 -07:00
Jon Austin
77080328c1 Issue 156 (#167)
* fix: Fixes #156, Added bucket name validation on bucket creation
2023-07-27 11:04:50 -07:00
Ben McClelland
b0259ae1de Merge pull request #166 from versity/ben/context 2023-07-27 06:44:43 -07:00
Ben McClelland
884fd029c3 feat: add context to backend calls
This adds a context to the backend interface calls so that the backend
can enable request cancellation. This change isn't acutally implementing
any backend handling, but just putting the pieces into place to pass the
context to the backend.
2023-07-26 21:54:12 -07:00
Ben McClelland
36eb6d795f Merge pull request #165 from versity/acl-checker-refactoring
ACL refactoring
2023-07-26 19:06:52 -07:00
Ben McClelland
7de01cc983 Merge pull request #163 from versity/ben/log_cleanup
fix: allow logging to user specified log files
2023-07-26 19:05:39 -07:00
jonaustin09
7fb2a7f9ba feat: ACL refactoring, moved ace parsing from controllers to middleware 2023-07-26 20:54:50 +04:00
Ben McClelland
5a9b744dd1 fix: allow logging to user specified log files
This also cleans up some the of the error output to send to stderr.

This adds the Shutdown() to the logging interface, so we can keep the
log file open and just append entries.

This add HangUp() to the logging interface for log rotations.
2023-07-25 23:39:45 -07:00
Ben McClelland
5b31a7bafc Merge pull request #162 from versity/fix/issue-136
Issue 136
2023-07-25 10:03:28 -07:00
Ben McClelland
ee703479d0 Merge pull request #161 from versity/fix/issue-150
Issue 150
2023-07-25 10:02:57 -07:00
Ben McClelland
bedd353d72 Merge pull request #160 from versity/fix/issue-155
Issue 155
2023-07-25 10:02:16 -07:00
Ben McClelland
84fe647b81 Merge pull request #159 from versity/fix/issue-157
Issue 157
2023-07-25 10:00:35 -07:00
jonaustin09
1649c5cafd fix: Added KeyCount property in ListObjectsV2 action result, added a test case for one 2023-07-25 20:44:57 +04:00
jonaustin09
4c451a4822 feat: Added support to add object tags on object creation 2023-07-25 20:42:58 +04:00
jonaustin09
287db7a7b6 fix: Fixed ListObjects marker bug, now it takes the correct query param as marker 2023-07-25 20:31:40 +04:00
jonaustin09
c598ee5416 fix: Added accept-range, Content-range and x-amz-tagging-count headers in GetObject action response, added test cases for these 2023-07-25 20:28:40 +04:00
Ben McClelland
7c08ea44a6 Merge pull request #149 from versity/ben/backend_interface
fix: standardize Backend interface args for s3 types
2023-07-24 08:26:56 -07:00
Ben McClelland
e73d661de1 Merge pull request #148 from versity/ben/admin_cleanup
fix: cleanup unused adminRegion
2023-07-24 08:26:45 -07:00
Ben McClelland
2291c22eaa fix: standardize Backend interface args for s3 types 2023-07-22 22:45:24 -07:00
Ben McClelland
51e818b3e3 fix: cleanup unused adminRegion 2023-07-22 18:53:58 -07:00
Ben McClelland
daa4aa1510 Merge pull request #135 from versity/ben/cleanup
fix: signal.go spelling
2023-07-20 14:03:21 -07:00
Ben McClelland
8765a6c67f fix: signal.go spelling 2023-07-20 13:59:51 -07:00
Ben McClelland
c5a7b5aae1 Merge pull request #134 from versity/event-notif-nats
feat: cleanup nats for kafka similarity
2023-07-20 13:58:10 -07:00
Ben McClelland
2ae39c3ee8 feat: cleanup nats for kafka similarity 2023-07-20 13:54:55 -07:00
Ben McClelland
d0b3139640 Merge pull request #133 from versity/event-notif-nats
Bucket event notifications(nats)
2023-07-20 13:50:53 -07:00
jonaustin09
7bceaaca39 feat: Set up bucket event notifications with nats 2023-07-20 13:36:16 -07:00
Ben McClelland
6f0f527e5f Merge pull request #132 from versity/event-notifications
Bucket event notifications(kafka)
2023-07-20 13:27:40 -07:00
jonaustin09
fe547a19e9 feat: bucket event notifications
Set up Bucket event notifications interface to send aws compatible format event messages to a configured event service.
First integrated service is kafka message broker as an option for bucket event notifications.
2023-07-20 11:37:14 -07:00
Ben McClelland
df7f01f7e2 Merge pull request #129 from versity/audit-logging-setup
feat: Set up audit logging basic structure, set up webhook logger, bu…
2023-07-14 12:50:32 -07:00
Ben McClelland
5aeb96f138 Merge pull request #131 from versity/fix-posix-delete-object
Fix Posix Delete Objects
2023-07-14 12:46:57 -07:00
jonaustin09
ef1de682a4 fix: Error handling for posix DeleteObject function to return an error when the object doesn't exist 2023-07-14 23:41:52 +04:00
jonaustin09
87d61a1eb3 feat: Setup audit loggin with webhook url and root level access.log file. CLI enables either webhook or server access logs by providing the flags 2023-07-14 23:40:05 +04:00
Ben McClelland
18899f8029 Merge pull request #128 from versity/ben/update
update package deps
2023-07-06 20:59:52 -07:00
Ben McClelland
ca28792458 update package deps 2023-07-06 21:56:59 -06:00
Ben McClelland
8c469cbd69 Merge pull request #127 from versity/ben/issue_templates
feat: add issue templates
2023-07-06 20:43:40 -07:00
Ben McClelland
ff4bf23b6b feat: add issue templates 2023-07-06 21:40:57 -06:00
56 changed files with 8433 additions and 2824 deletions

27
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View 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.

View 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.

10
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,10 @@
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "weekly"
groups:
dev-dependencies:
patterns:
- "*"

View File

@@ -7,15 +7,15 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v3
- 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 ./...

View File

@@ -7,15 +7,15 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v3
- 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: Verify all files pass gofmt formatting
run: if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then gofmt -s -d .; exit 1; fi

31
.github/workflows/goreleaser.yml vendored Normal file
View File

@@ -0,0 +1,31 @@
name: goreleaser
on:
push:
# run only against tags
tags:
- '*'
permissions:
contents: write
# packages: write
# issues: write
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- run: git fetch --force --tags
- uses: actions/setup-go@v4
with:
go-version: stable
- uses: goreleaser/goreleaser-action@v4
with:
distribution: goreleaser
version: latest
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.TOKEN }}

View File

@@ -1,23 +1,22 @@
name: staticcheck
on: pull_request
jobs:
build:
name: Check
runs-on: ubuntu-latest
steps:
-
name: Set up Go
uses: actions/setup-go@v2
with:
go-version: "1.20"
id: go
-
name: "Set up repo"
uses: actions/checkout@v1
with:
fetch-depth: 1
-
name: "staticcheck"
uses: dominikh/staticcheck-action@v1.3.0
with:
install-go: false
- name: Check out code into the Go module directory
uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: 'stable'
id: go
- name: "staticcheck"
uses: dominikh/staticcheck-action@v1.3.0
with:
install-go: false

2
.gitignore vendored
View File

@@ -34,3 +34,5 @@ VERSION
*.tar.gz
**/rand.data
/profile.txt
dist/

51
.goreleaser.yaml Normal file
View File

@@ -0,0 +1,51 @@
before:
hooks:
- go mod tidy
builds:
- goos:
- linux
- darwin
# windows is untested, we can start doing windows releases
# if someone is interested in taking on testing
# - windows
main: ./cmd/versitygw
binary: ./cmd/versitygw
id: versitygw
goarch:
- amd64
- arm64
ldflags:
- -X=main.Build={{.Commit}} -X=main.BuildTime={{.Date}} -X=main.Version={{.Version}}
archives:
- format: tar.gz
# this name template makes the OS and Arch compatible with the results of uname.
name_template: >-
{{ .ProjectName }}_
{{- title .Os }}_
{{- if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "386" }}i386
{{- else }}{{ .Arch }}{{ end }}
{{- if .Arm }}v{{ .Arm }}{{ end }}
# use zip for windows archives
format_overrides:
- goos: windows
format: zip
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: "{{ incpatch .Version }}-next"
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'
- '^Merge '
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
# vim: set ts=2 sw=2 tw=0 fo=cnqoj

View File

@@ -1,4 +1,4 @@
# The Versity Gateway:<br/>A High-Performance S3 to Storage System Translation Service
# The Versity S3 Gateway:<br/>A High-Performance S3 Translation Service
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/versity/versitygw/blob/assets/assets/logo-white.svg">
@@ -8,10 +8,14 @@
[![Apache V2 License](https://img.shields.io/badge/license-Apache%20V2-blue.svg)](https://github.com/versity/versitygw/blob/main/LICENSE)
**Current status:** Alpha, in development not yet suited for production use
**Current status:** Beta: Most clients functional, work in progress for more test coverage. Issue reports welcome.
See project [documentation](https://github.com/versity/versitygw/wiki) on the wiki.
* Share filesystem directory via S3 protocol
* Simple to deploy S3 server with a single command
* Protocol compatibility allows common access to files via posix or S3
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.

View File

@@ -105,7 +105,6 @@ func UpdateACL(input *s3.PutBucketAclInput, acl ACL, iam IAMService) ([]byte, er
if *input.GrantFullControl != "" {
fullControlList = splitUnique(*input.GrantFullControl, ",")
fmt.Println(fullControlList)
for _, str := range fullControlList {
grantees = append(grantees, Grantee{Access: str, Permission: "FULL_CONTROL"})
}
@@ -148,7 +147,7 @@ func UpdateACL(input *s3.PutBucketAclInput, acl ACL, iam IAMService) ([]byte, er
}
// Check if the specified accounts exist
accList, err := checkIfAccountsExist(accs, iam)
accList, err := CheckIfAccountsExist(accs, iam)
if err != nil {
return nil, err
}
@@ -168,7 +167,7 @@ func UpdateACL(input *s3.PutBucketAclInput, acl ACL, iam IAMService) ([]byte, er
return result, nil
}
func checkIfAccountsExist(accs []string, iam IAMService) ([]string, error) {
func CheckIfAccountsExist(accs []string, iam IAMService) ([]string, error) {
result := []string{}
for _, acc := range accs {
@@ -261,5 +260,5 @@ func IsAdmin(access string, isRoot bool) error {
if acc.Role == "admin" {
return nil
}
return fmt.Errorf("only admin users have access to this resource")
return s3err.GetAPIError(s3err.ErrAccessDenied)
}

View File

@@ -20,6 +20,7 @@ import (
// Account is a gateway IAM account
type Account struct {
Access string `json:"access"`
Secret string `json:"secret"`
Role string `json:"role"`
}
@@ -31,6 +32,7 @@ type IAMService interface {
CreateAccount(access string, account Account) error
GetUserAccount(access string) (Account, error)
DeleteUserAccount(access string) error
ListUserAccounts() ([]Account, error)
}
var ErrNoSuchUser = errors.New("user not found")

View File

@@ -179,3 +179,34 @@ func (s *IAMServiceInternal) DeleteUserAccount(access string) error {
return b, nil
})
}
// ListUserAccounts lists all the user accounts stored.
func (s *IAMServiceInternal) ListUserAccounts() (accs []Account, err error) {
s.mu.RLock()
defer s.mu.RUnlock()
data, err := s.storer.GetIAM()
if err != nil {
return []Account{}, fmt.Errorf("get iam data: %w", err)
}
serial := crc32.ChecksumIEEE(data)
if serial != s.serial {
s.mu.RUnlock()
err := s.updateCache()
s.mu.RLock()
if err != nil {
return []Account{}, fmt.Errorf("refresh iam cache: %w", err)
}
}
for access, usr := range s.accts.AccessAccounts {
accs = append(accs, Account{
Access: access,
Secret: usr.Secret,
Role: usr.Role,
})
}
return accs, nil
}

View File

@@ -15,11 +15,11 @@
package backend
import (
"context"
"fmt"
"io"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/versity/versitygw/s3err"
"github.com/versity/versitygw/s3response"
)
@@ -29,37 +29,48 @@ type Backend interface {
fmt.Stringer
Shutdown()
ListBuckets() (s3response.ListAllMyBucketsResult, error)
HeadBucket(bucket string) (*s3.HeadBucketOutput, error)
GetBucketAcl(bucket string) ([]byte, error)
PutBucket(bucket, owner string) error
PutBucketAcl(bucket string, data []byte) error
DeleteBucket(bucket string) error
// bucket operations
ListBuckets(_ context.Context, owner string, isAdmin bool) (s3response.ListAllMyBucketsResult, error)
HeadBucket(context.Context, *s3.HeadBucketInput) (*s3.HeadBucketOutput, error)
GetBucketAcl(context.Context, *s3.GetBucketAclInput) ([]byte, error)
CreateBucket(context.Context, *s3.CreateBucketInput) error
PutBucketAcl(_ context.Context, bucket string, data []byte) error
DeleteBucket(context.Context, *s3.DeleteBucketInput) error
CreateMultipartUpload(*s3.CreateMultipartUploadInput) (*s3.CreateMultipartUploadOutput, error)
CompleteMultipartUpload(bucket, object, uploadID string, parts []types.Part) (*s3.CompleteMultipartUploadOutput, error)
AbortMultipartUpload(*s3.AbortMultipartUploadInput) error
ListMultipartUploads(*s3.ListMultipartUploadsInput) (s3response.ListMultipartUploadsResponse, error)
ListObjectParts(bucket, object, uploadID string, partNumberMarker int, maxParts int) (s3response.ListPartsResponse, error)
PutObjectPart(bucket, object, uploadID string, part int, length int64, r io.Reader) (etag string, err error)
UploadPartCopy(*s3.UploadPartCopyInput) (s3response.CopyObjectResult, error)
// multipart operations
CreateMultipartUpload(context.Context, *s3.CreateMultipartUploadInput) (*s3.CreateMultipartUploadOutput, error)
CompleteMultipartUpload(context.Context, *s3.CompleteMultipartUploadInput) (*s3.CompleteMultipartUploadOutput, error)
AbortMultipartUpload(context.Context, *s3.AbortMultipartUploadInput) error
ListMultipartUploads(context.Context, *s3.ListMultipartUploadsInput) (s3response.ListMultipartUploadsResult, error)
ListParts(context.Context, *s3.ListPartsInput) (s3response.ListPartsResult, error)
UploadPart(context.Context, *s3.UploadPartInput) (etag string, err error)
UploadPartCopy(context.Context, *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)
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
DeleteObjects(bucket string, objects *s3.DeleteObjectsInput) error
PutObjectAcl(*s3.PutObjectAclInput) error
RestoreObject(bucket, object string, restoreRequest *s3.RestoreObjectInput) error
// standard object operations
PutObject(context.Context, *s3.PutObjectInput) (string, error)
HeadObject(context.Context, *s3.HeadObjectInput) (*s3.HeadObjectOutput, error)
GetObject(context.Context, *s3.GetObjectInput, io.Writer) (*s3.GetObjectOutput, error)
GetObjectAcl(context.Context, *s3.GetObjectAclInput) (*s3.GetObjectAclOutput, error)
GetObjectAttributes(context.Context, *s3.GetObjectAttributesInput) (*s3.GetObjectAttributesOutput, error)
CopyObject(context.Context, *s3.CopyObjectInput) (*s3.CopyObjectOutput, error)
ListObjects(context.Context, *s3.ListObjectsInput) (*s3.ListObjectsOutput, error)
ListObjectsV2(context.Context, *s3.ListObjectsV2Input) (*s3.ListObjectsV2Output, error)
DeleteObject(context.Context, *s3.DeleteObjectInput) error
DeleteObjects(context.Context, *s3.DeleteObjectsInput) (s3response.DeleteObjectsResult, error)
PutObjectAcl(context.Context, *s3.PutObjectAclInput) error
GetTags(bucket, object string) (map[string]string, error)
SetTags(bucket, object string, tags map[string]string) error
RemoveTags(bucket, object string) error
// special case object operations
RestoreObject(context.Context, *s3.RestoreObjectInput) error
SelectObjectContent(context.Context, *s3.SelectObjectContentInput) (s3response.SelectObjectContentResult, error)
// object tags operations
GetTags(_ context.Context, bucket, object string) (map[string]string, error)
SetTags(_ context.Context, bucket, object string, tags map[string]string) error
RemoveTags(_ context.Context, bucket, object string) error
// non AWS actions
ChangeBucketOwner(_ context.Context, bucket, newOwner string) error
ListBucketsAndOwners(context.Context) ([]s3response.Bucket, error)
}
type BackendUnsupported struct{}
@@ -73,90 +84,101 @@ func (BackendUnsupported) Shutdown() {}
func (BackendUnsupported) String() string {
return "Unsupported"
}
func (BackendUnsupported) ListBuckets() (s3response.ListAllMyBucketsResult, error) {
func (BackendUnsupported) ListBuckets(context.Context, string, bool) (s3response.ListAllMyBucketsResult, error) {
return s3response.ListAllMyBucketsResult{}, s3err.GetAPIError(s3err.ErrNotImplemented)
}
func (BackendUnsupported) PutBucketAcl(bucket string, data []byte) error {
func (BackendUnsupported) HeadBucket(context.Context, *s3.HeadBucketInput) (*s3.HeadBucketOutput, error) {
return nil, s3err.GetAPIError(s3err.ErrNotImplemented)
}
func (BackendUnsupported) GetBucketAcl(context.Context, *s3.GetBucketAclInput) ([]byte, error) {
return nil, s3err.GetAPIError(s3err.ErrNotImplemented)
}
func (BackendUnsupported) CreateBucket(context.Context, *s3.CreateBucketInput) error {
return s3err.GetAPIError(s3err.ErrNotImplemented)
}
func (BackendUnsupported) PutObjectAcl(*s3.PutObjectAclInput) error {
func (BackendUnsupported) PutBucketAcl(_ context.Context, bucket string, data []byte) error {
return s3err.GetAPIError(s3err.ErrNotImplemented)
}
func (BackendUnsupported) RestoreObject(bucket, object string, restoreRequest *s3.RestoreObjectInput) error {
func (BackendUnsupported) DeleteBucket(context.Context, *s3.DeleteBucketInput) error {
return s3err.GetAPIError(s3err.ErrNotImplemented)
}
func (BackendUnsupported) UploadPartCopy(*s3.UploadPartCopyInput) (s3response.CopyObjectResult, error) {
func (BackendUnsupported) CreateMultipartUpload(context.Context, *s3.CreateMultipartUploadInput) (*s3.CreateMultipartUploadOutput, error) {
return nil, s3err.GetAPIError(s3err.ErrNotImplemented)
}
func (BackendUnsupported) CompleteMultipartUpload(context.Context, *s3.CompleteMultipartUploadInput) (*s3.CompleteMultipartUploadOutput, error) {
return nil, s3err.GetAPIError(s3err.ErrNotImplemented)
}
func (BackendUnsupported) AbortMultipartUpload(context.Context, *s3.AbortMultipartUploadInput) error {
return s3err.GetAPIError(s3err.ErrNotImplemented)
}
func (BackendUnsupported) ListMultipartUploads(context.Context, *s3.ListMultipartUploadsInput) (s3response.ListMultipartUploadsResult, error) {
return s3response.ListMultipartUploadsResult{}, s3err.GetAPIError(s3err.ErrNotImplemented)
}
func (BackendUnsupported) ListParts(context.Context, *s3.ListPartsInput) (s3response.ListPartsResult, error) {
return s3response.ListPartsResult{}, s3err.GetAPIError(s3err.ErrNotImplemented)
}
func (BackendUnsupported) UploadPart(context.Context, *s3.UploadPartInput) (etag string, err error) {
return "", s3err.GetAPIError(s3err.ErrNotImplemented)
}
func (BackendUnsupported) UploadPartCopy(context.Context, *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)
}
func (BackendUnsupported) HeadBucket(bucket string) (*s3.HeadBucketOutput, error) {
return nil, s3err.GetAPIError(s3err.ErrNotImplemented)
}
func (BackendUnsupported) PutBucket(bucket, owner string) error {
return s3err.GetAPIError(s3err.ErrNotImplemented)
}
func (BackendUnsupported) DeleteBucket(bucket string) error {
return s3err.GetAPIError(s3err.ErrNotImplemented)
}
func (BackendUnsupported) CreateMultipartUpload(input *s3.CreateMultipartUploadInput) (*s3.CreateMultipartUploadOutput, error) {
return nil, s3err.GetAPIError(s3err.ErrNotImplemented)
}
func (BackendUnsupported) CompleteMultipartUpload(bucket, object, uploadID string, parts []types.Part) (*s3.CompleteMultipartUploadOutput, error) {
return nil, s3err.GetAPIError(s3err.ErrNotImplemented)
}
func (BackendUnsupported) AbortMultipartUpload(input *s3.AbortMultipartUploadInput) error {
return s3err.GetAPIError(s3err.ErrNotImplemented)
}
func (BackendUnsupported) ListMultipartUploads(output *s3.ListMultipartUploadsInput) (s3response.ListMultipartUploadsResponse, error) {
return s3response.ListMultipartUploadsResponse{}, s3err.GetAPIError(s3err.ErrNotImplemented)
}
func (BackendUnsupported) ListObjectParts(bucket, object, uploadID string, partNumberMarker int, maxParts int) (s3response.ListPartsResponse, error) {
return s3response.ListPartsResponse{}, s3err.GetAPIError(s3err.ErrNotImplemented)
}
func (BackendUnsupported) PutObjectPart(bucket, object, uploadID string, part int, length int64, r io.Reader) (etag string, err error) {
func (BackendUnsupported) PutObject(context.Context, *s3.PutObjectInput) (string, error) {
return "", s3err.GetAPIError(s3err.ErrNotImplemented)
}
func (BackendUnsupported) PutObject(*s3.PutObjectInput) (string, error) {
return "", s3err.GetAPIError(s3err.ErrNotImplemented)
func (BackendUnsupported) HeadObject(context.Context, *s3.HeadObjectInput) (*s3.HeadObjectOutput, error) {
return nil, s3err.GetAPIError(s3err.ErrNotImplemented)
}
func (BackendUnsupported) DeleteObject(bucket, object string) error {
func (BackendUnsupported) GetObject(context.Context, *s3.GetObjectInput, io.Writer) (*s3.GetObjectOutput, error) {
return nil, s3err.GetAPIError(s3err.ErrNotImplemented)
}
func (BackendUnsupported) GetObjectAcl(context.Context, *s3.GetObjectAclInput) (*s3.GetObjectAclOutput, error) {
return nil, s3err.GetAPIError(s3err.ErrNotImplemented)
}
func (BackendUnsupported) GetObjectAttributes(context.Context, *s3.GetObjectAttributesInput) (*s3.GetObjectAttributesOutput, error) {
return nil, s3err.GetAPIError(s3err.ErrNotImplemented)
}
func (BackendUnsupported) CopyObject(context.Context, *s3.CopyObjectInput) (*s3.CopyObjectOutput, error) {
return nil, s3err.GetAPIError(s3err.ErrNotImplemented)
}
func (BackendUnsupported) ListObjects(context.Context, *s3.ListObjectsInput) (*s3.ListObjectsOutput, error) {
return nil, s3err.GetAPIError(s3err.ErrNotImplemented)
}
func (BackendUnsupported) ListObjectsV2(context.Context, *s3.ListObjectsV2Input) (*s3.ListObjectsV2Output, error) {
return nil, s3err.GetAPIError(s3err.ErrNotImplemented)
}
func (BackendUnsupported) DeleteObject(context.Context, *s3.DeleteObjectInput) error {
return s3err.GetAPIError(s3err.ErrNotImplemented)
}
func (BackendUnsupported) DeleteObjects(bucket string, objects *s3.DeleteObjectsInput) error {
func (BackendUnsupported) DeleteObjects(context.Context, *s3.DeleteObjectsInput) (s3response.DeleteObjectsResult, error) {
return s3response.DeleteObjectsResult{}, s3err.GetAPIError(s3err.ErrNotImplemented)
}
func (BackendUnsupported) PutObjectAcl(context.Context, *s3.PutObjectAclInput) error {
return s3err.GetAPIError(s3err.ErrNotImplemented)
}
func (BackendUnsupported) GetObject(bucket, object, acceptRange string, writer io.Writer) (*s3.GetObjectOutput, error) {
return nil, s3err.GetAPIError(s3err.ErrNotImplemented)
}
func (BackendUnsupported) HeadObject(bucket, object string) (*s3.HeadObjectOutput, error) {
return nil, s3err.GetAPIError(s3err.ErrNotImplemented)
}
func (BackendUnsupported) GetObjectAcl(bucket, object string) (*s3.GetObjectAclOutput, error) {
return nil, s3err.GetAPIError(s3err.ErrNotImplemented)
}
func (BackendUnsupported) GetObjectAttributes(bucket, object string, attributes []string) (*s3.GetObjectAttributesOutput, error) {
return nil, s3err.GetAPIError(s3err.ErrNotImplemented)
}
func (BackendUnsupported) CopyObject(srcBucket, srcObject, DstBucket, dstObject string) (*s3.CopyObjectOutput, error) {
return nil, s3err.GetAPIError(s3err.ErrNotImplemented)
}
func (BackendUnsupported) ListObjects(bucket, prefix, marker, delim string, maxkeys int) (*s3.ListObjectsOutput, error) {
return nil, s3err.GetAPIError(s3err.ErrNotImplemented)
}
func (BackendUnsupported) ListObjectsV2(bucket, prefix, marker, delim string, maxkeys int) (*s3.ListObjectsV2Output, error) {
return nil, s3err.GetAPIError(s3err.ErrNotImplemented)
}
func (BackendUnsupported) GetTags(bucket, object string) (map[string]string, error) {
func (BackendUnsupported) RestoreObject(context.Context, *s3.RestoreObjectInput) error {
return s3err.GetAPIError(s3err.ErrNotImplemented)
}
func (BackendUnsupported) SelectObjectContent(context.Context, *s3.SelectObjectContentInput) (s3response.SelectObjectContentResult, error) {
return s3response.SelectObjectContentResult{}, s3err.GetAPIError(s3err.ErrNotImplemented)
}
func (BackendUnsupported) GetTags(_ context.Context, bucket, object string) (map[string]string, error) {
return nil, s3err.GetAPIError(s3err.ErrNotImplemented)
}
func (BackendUnsupported) SetTags(bucket, object string, tags map[string]string) error {
func (BackendUnsupported) SetTags(_ context.Context, bucket, object string, tags map[string]string) error {
return s3err.GetAPIError(s3err.ErrNotImplemented)
}
func (BackendUnsupported) RemoveTags(bucket, object string) error {
func (BackendUnsupported) RemoveTags(_ context.Context, bucket, object string) error {
return s3err.GetAPIError(s3err.ErrNotImplemented)
}
func (BackendUnsupported) ChangeBucketOwner(_ context.Context, bucket, newOwner string) error {
return s3err.GetAPIError(s3err.ErrNotImplemented)
}
func (BackendUnsupported) ListBucketsAndOwners(context.Context) ([]s3response.Bucket, error) {
return []s3response.Bucket{}, s3err.GetAPIError(s3err.ErrNotImplemented)
}

View File

@@ -56,7 +56,7 @@ func GetTimePtr(t time.Time) *time.Time {
}
var (
errInvalidRange = s3err.GetAPIError(s3err.ErrInvalidRequest)
errInvalidRange = s3err.GetAPIError(s3err.ErrInvalidRange)
)
// ParseRange parses input range header and returns startoffset, length, and
@@ -99,7 +99,7 @@ func ParseRange(file fs.FileInfo, acceptRange string) (int64, int64, error) {
return startOffset, endOffset - startOffset + 1, nil
}
func GetMultipartMD5(parts []types.Part) string {
func GetMultipartMD5(parts []types.CompletedPart) string {
var partsEtagBytes []byte
for _, part := range parts {
partsEtagBytes = append(partsEtagBytes, getEtagBytes(*part.ETag)...)

View File

@@ -15,6 +15,7 @@
package posix
import (
"context"
"crypto/md5"
"crypto/sha256"
"encoding/hex"
@@ -65,6 +66,7 @@ const (
metaTmpMultipartDir = metaTmpDir + "/multipart"
onameAttr = "user.objname"
tagHdr = "X-Amz-Tagging"
metaHdr = "X-Amz-Meta"
contentTypeHdr = "content-type"
contentEncHdr = "content-encoding"
emptyMD5 = "d41d8cd98f00b204e9800998ecf8427e"
@@ -96,7 +98,7 @@ func (p *Posix) String() string {
return "Posix Gateway"
}
func (p *Posix) ListBuckets() (s3response.ListAllMyBucketsResult, error) {
func (p *Posix) ListBuckets(_ context.Context, owner string, isAdmin bool) (s3response.ListAllMyBucketsResult, error) {
entries, err := os.ReadDir(".")
if err != nil {
return s3response.ListAllMyBucketsResult{},
@@ -116,10 +118,32 @@ func (p *Posix) ListBuckets() (s3response.ListAllMyBucketsResult, error) {
continue
}
buckets = append(buckets, s3response.ListAllMyBucketsEntry{
Name: entry.Name(),
CreationDate: fi.ModTime(),
})
// return all the buckets for admin users
if isAdmin {
buckets = append(buckets, s3response.ListAllMyBucketsEntry{
Name: entry.Name(),
CreationDate: fi.ModTime(),
})
continue
}
aclTag, err := xattr.Get(entry.Name(), aclkey)
if err != nil {
return s3response.ListAllMyBucketsResult{}, fmt.Errorf("get acl tag: %w", err)
}
var acl auth.ACL
err = json.Unmarshal(aclTag, &acl)
if err != nil {
return s3response.ListAllMyBucketsResult{}, fmt.Errorf("parse acl tag: %w", err)
}
if acl.Owner == owner {
buckets = append(buckets, s3response.ListAllMyBucketsEntry{
Name: entry.Name(),
CreationDate: fi.ModTime(),
})
}
}
sort.Sort(backend.ByBucketName(buckets))
@@ -128,11 +152,14 @@ func (p *Posix) ListBuckets() (s3response.ListAllMyBucketsResult, error) {
Buckets: s3response.ListAllMyBucketsList{
Bucket: buckets,
},
Owner: s3response.CanonicalUser{
ID: owner,
},
}, nil
}
func (p *Posix) HeadBucket(bucket string) (*s3.HeadBucketOutput, error) {
_, err := os.Lstat(bucket)
func (p *Posix) HeadBucket(_ context.Context, input *s3.HeadBucketInput) (*s3.HeadBucketOutput, error) {
_, err := os.Lstat(*input.Bucket)
if errors.Is(err, fs.ErrNotExist) {
return nil, s3err.GetAPIError(s3err.ErrNoSuchBucket)
}
@@ -143,7 +170,10 @@ func (p *Posix) HeadBucket(bucket string) (*s3.HeadBucketOutput, error) {
return &s3.HeadBucketOutput{}, nil
}
func (p *Posix) PutBucket(bucket string, owner string) error {
func (p *Posix) CreateBucket(_ context.Context, input *s3.CreateBucketInput) error {
bucket := *input.Bucket
owner := string(input.ObjectOwnership)
err := os.Mkdir(bucket, 0777)
if err != nil && os.IsExist(err) {
return s3err.GetAPIError(s3err.ErrBucketAlreadyExists)
@@ -165,8 +195,8 @@ func (p *Posix) PutBucket(bucket string, owner string) error {
return nil
}
func (p *Posix) DeleteBucket(bucket string) error {
names, err := os.ReadDir(bucket)
func (p *Posix) DeleteBucket(_ context.Context, input *s3.DeleteBucketInput) error {
names, err := os.ReadDir(*input.Bucket)
if errors.Is(err, fs.ErrNotExist) {
return s3err.GetAPIError(s3err.ErrNoSuchBucket)
}
@@ -177,13 +207,13 @@ func (p *Posix) DeleteBucket(bucket string) error {
if len(names) == 1 && names[0].Name() == metaTmpDir {
// if .sgwtmp is only item in directory
// then clean this up before trying to remove the bucket
err = os.RemoveAll(filepath.Join(bucket, metaTmpDir))
err = os.RemoveAll(filepath.Join(*input.Bucket, metaTmpDir))
if err != nil && !errors.Is(err, fs.ErrNotExist) {
return fmt.Errorf("remove temp dir: %w", err)
}
}
err = os.Remove(bucket)
err = os.Remove(*input.Bucket)
if err != nil && err.(*os.PathError).Err == syscall.ENOTEMPTY {
return s3err.GetAPIError(s3err.ErrBucketNotEmpty)
}
@@ -194,7 +224,7 @@ func (p *Posix) DeleteBucket(bucket string) error {
return nil
}
func (p *Posix) CreateMultipartUpload(mpu *s3.CreateMultipartUploadInput) (*s3.CreateMultipartUploadOutput, error) {
func (p *Posix) CreateMultipartUpload(_ context.Context, mpu *s3.CreateMultipartUploadInput) (*s3.CreateMultipartUploadOutput, error) {
bucket := *mpu.Bucket
object := *mpu.Key
@@ -245,7 +275,12 @@ func (p *Posix) CreateMultipartUpload(mpu *s3.CreateMultipartUploadInput) (*s3.C
}, nil
}
func (p *Posix) CompleteMultipartUpload(bucket, object, uploadID string, parts []types.Part) (*s3.CompleteMultipartUploadOutput, error) {
func (p *Posix) CompleteMultipartUpload(_ context.Context, input *s3.CompleteMultipartUploadInput) (*s3.CompleteMultipartUploadOutput, error) {
bucket := *input.Bucket
object := *input.Key
uploadID := *input.UploadId
parts := input.MultipartUpload.Parts
_, err := os.Stat(bucket)
if errors.Is(err, fs.ErrNotExist) {
return nil, s3err.GetAPIError(s3err.ErrNoSuchBucket)
@@ -286,7 +321,9 @@ func (p *Posix) CompleteMultipartUpload(bucket, object, uploadID string, parts [
if err != nil {
etag = ""
}
parts[i].ETag = &etag
if etag != *parts[i].ETag {
return nil, s3err.GetAPIError(s3err.ErrInvalidPart)
}
}
f, err := openTmpFile(filepath.Join(bucket, metaTmpDir), bucket, object, totalsize)
@@ -382,13 +419,13 @@ func loadUserMetaData(path string, m map[string]string) (contentType, contentEnc
}
b, err := xattr.Get(path, e)
if err == syscall.ENODATA {
m[strings.TrimPrefix(e, "user.")] = ""
m[strings.TrimPrefix(e, fmt.Sprintf("user.%v.", metaHdr))] = ""
continue
}
if err != nil {
continue
}
m[strings.TrimPrefix(e, "user.")] = string(b)
m[strings.TrimPrefix(e, fmt.Sprintf("user.%v.", metaHdr))] = string(b)
}
b, err := xattr.Get(path, "user."+contentTypeHdr)
@@ -413,7 +450,7 @@ func loadUserMetaData(path string, m map[string]string) (contentType, contentEnc
}
func isValidMeta(val string) bool {
if strings.HasPrefix(val, "user.X-Amz-Meta") {
if strings.HasPrefix(val, "user."+metaHdr) {
return true
}
if strings.EqualFold(val, "user.Expires") {
@@ -467,7 +504,7 @@ func mkdirAll(path string, perm os.FileMode, bucket, object string) error {
return nil
}
func (p *Posix) AbortMultipartUpload(mpu *s3.AbortMultipartUploadInput) error {
func (p *Posix) AbortMultipartUpload(_ context.Context, mpu *s3.AbortMultipartUploadInput) error {
bucket := *mpu.Bucket
object := *mpu.Key
uploadID := *mpu.UploadId
@@ -497,7 +534,7 @@ func (p *Posix) AbortMultipartUpload(mpu *s3.AbortMultipartUploadInput) error {
return nil
}
func (p *Posix) ListMultipartUploads(mpu *s3.ListMultipartUploadsInput) (s3response.ListMultipartUploadsResponse, error) {
func (p *Posix) ListMultipartUploads(_ context.Context, mpu *s3.ListMultipartUploadsInput) (s3response.ListMultipartUploadsResult, error) {
bucket := *mpu.Bucket
var delimiter string
if mpu.Delimiter != nil {
@@ -508,7 +545,7 @@ func (p *Posix) ListMultipartUploads(mpu *s3.ListMultipartUploadsInput) (s3respo
prefix = *mpu.Prefix
}
var lmu s3response.ListMultipartUploadsResponse
var lmu s3response.ListMultipartUploadsResult
_, err := os.Stat(bucket)
if errors.Is(err, fs.ErrNotExist) {
@@ -522,6 +559,7 @@ func (p *Posix) ListMultipartUploads(mpu *s3.ListMultipartUploadsInput) (s3respo
objs, _ := os.ReadDir(filepath.Join(bucket, metaTmpMultipartDir))
var uploads []s3response.Upload
var resultUpds []s3response.Upload
var keyMarker string
if mpu.KeyMarker != nil {
@@ -531,12 +569,9 @@ func (p *Posix) ListMultipartUploads(mpu *s3.ListMultipartUploadsInput) (s3respo
if mpu.UploadIdMarker != nil {
uploadIDMarker = *mpu.UploadIdMarker
}
var pastMarker bool
if keyMarker == "" && uploadIDMarker == "" {
pastMarker = true
}
keyMarkerInd, uploadIdMarkerFound := -1, false
for i, obj := range objs {
for _, obj := range objs {
if !obj.IsDir() {
continue
}
@@ -555,22 +590,14 @@ func (p *Posix) ListMultipartUploads(mpu *s3.ListMultipartUploadsInput) (s3respo
continue
}
for j, upid := range upids {
for _, upid := range upids {
if !upid.IsDir() {
continue
}
if objectName == keyMarker || upid.Name() == uploadIDMarker {
pastMarker = true
continue
}
if keyMarker != "" && uploadIDMarker != "" && !pastMarker {
continue
}
userMetaData := make(map[string]string)
upiddir := filepath.Join(bucket, metaTmpMultipartDir, obj.Name(), upid.Name())
loadUserMetaData(upiddir, userMetaData)
// userMetaData := make(map[string]string)
// upiddir := filepath.Join(bucket, metaTmpMultipartDir, obj.Name(), upid.Name())
// loadUserMetaData(upiddir, userMetaData)
fi, err := upid.Info()
if err != nil {
@@ -578,41 +605,90 @@ func (p *Posix) ListMultipartUploads(mpu *s3.ListMultipartUploadsInput) (s3respo
}
uploadID := upid.Name()
if !uploadIdMarkerFound && uploadIDMarker == uploadID {
uploadIdMarkerFound = true
}
if keyMarkerInd == -1 && objectName == keyMarker {
keyMarkerInd = len(uploads)
}
uploads = append(uploads, s3response.Upload{
Key: objectName,
UploadID: uploadID,
Initiated: fi.ModTime().Format(backend.RFC3339TimeFormat),
})
if len(uploads) == int(mpu.MaxUploads) {
return s3response.ListMultipartUploadsResponse{
Bucket: bucket,
Delimiter: delimiter,
IsTruncated: i != len(objs) || j != len(upids),
KeyMarker: keyMarker,
MaxUploads: int(mpu.MaxUploads),
NextKeyMarker: objectName,
NextUploadIDMarker: uploadID,
Prefix: prefix,
UploadIDMarker: uploadIDMarker,
Uploads: uploads,
}, nil
}
}
}
return s3response.ListMultipartUploadsResponse{
if (uploadIDMarker != "" && !uploadIdMarkerFound) || (keyMarker != "" && keyMarkerInd == -1) {
return s3response.ListMultipartUploadsResult{
Bucket: bucket,
Delimiter: delimiter,
KeyMarker: keyMarker,
MaxUploads: int(mpu.MaxUploads),
Prefix: prefix,
UploadIDMarker: uploadIDMarker,
Uploads: []s3response.Upload{},
}, nil
}
sort.SliceStable(uploads, func(i, j int) bool {
return uploads[i].Key < uploads[j].Key
})
for i := keyMarkerInd + 1; i < len(uploads); i++ {
if mpu.MaxUploads == 0 {
break
}
if keyMarker != "" && uploadIDMarker != "" && uploads[i].UploadID < uploadIDMarker {
continue
}
if i != len(uploads)-1 && len(resultUpds) == int(mpu.MaxUploads) {
return s3response.ListMultipartUploadsResult{
Bucket: bucket,
Delimiter: delimiter,
KeyMarker: keyMarker,
MaxUploads: int(mpu.MaxUploads),
NextKeyMarker: resultUpds[i-1].Key,
NextUploadIDMarker: resultUpds[i-1].UploadID,
IsTruncated: true,
Prefix: prefix,
UploadIDMarker: uploadIDMarker,
Uploads: resultUpds,
}, nil
}
resultUpds = append(resultUpds, uploads[i])
}
return s3response.ListMultipartUploadsResult{
Bucket: bucket,
Delimiter: delimiter,
KeyMarker: keyMarker,
MaxUploads: int(mpu.MaxUploads),
Prefix: prefix,
UploadIDMarker: uploadIDMarker,
Uploads: uploads,
Uploads: resultUpds,
}, nil
}
func (p *Posix) ListObjectParts(bucket, object, uploadID string, partNumberMarker int, maxParts int) (s3response.ListPartsResponse, error) {
var lpr s3response.ListPartsResponse
func (p *Posix) ListParts(_ context.Context, input *s3.ListPartsInput) (s3response.ListPartsResult, error) {
bucket := *input.Bucket
object := *input.Key
uploadID := *input.UploadId
stringMarker := *input.PartNumberMarker
maxParts := int(input.MaxParts)
var lpr s3response.ListPartsResult
var partNumberMarker int
if stringMarker != "" {
var err error
partNumberMarker, err = strconv.Atoi(stringMarker)
if err != nil {
return lpr, s3err.GetAPIError(s3err.ErrInvalidPartNumberMarker)
}
}
_, err := os.Stat(bucket)
if errors.Is(err, fs.ErrNotExist) {
return lpr, s3err.GetAPIError(s3err.ErrNoSuchBucket)
@@ -681,7 +757,7 @@ func (p *Posix) ListObjectParts(bucket, object, uploadID string, partNumberMarke
upiddir := filepath.Join(objdir, uploadID)
loadUserMetaData(upiddir, userMetaData)
return s3response.ListPartsResponse{
return s3response.ListPartsResult{
Bucket: bucket,
IsTruncated: oldLen != newLen,
Key: object,
@@ -693,7 +769,14 @@ func (p *Posix) ListObjectParts(bucket, object, uploadID string, partNumberMarke
}, nil
}
func (p *Posix) PutObjectPart(bucket, object, uploadID string, part int, length int64, r io.Reader) (string, error) {
func (p *Posix) UploadPart(_ context.Context, input *s3.UploadPartInput) (string, error) {
bucket := *input.Bucket
object := *input.Key
uploadID := *input.UploadId
part := input.PartNumber
length := input.ContentLength
r := input.Body
_, err := os.Stat(bucket)
if errors.Is(err, fs.ErrNotExist) {
return "", s3err.GetAPIError(s3err.ErrNoSuchBucket)
@@ -720,7 +803,6 @@ func (p *Posix) PutObjectPart(bucket, object, uploadID string, part int, length
if err != nil {
return "", fmt.Errorf("open temp file: %w", err)
}
defer f.cleanup()
hash := md5.New()
tr := io.TeeReader(r, hash)
@@ -734,14 +816,16 @@ func (p *Posix) PutObjectPart(bucket, object, uploadID string, part int, length
return "", fmt.Errorf("link object in namespace: %w", err)
}
f.cleanup()
dataSum := hash.Sum(nil)
etag := hex.EncodeToString(dataSum)
xattr.Set(partPath, etagkey, []byte(etag))
xattr.Set(filepath.Join(bucket, partPath), etagkey, []byte(etag))
return etag, nil
}
func (p *Posix) UploadPartCopy(upi *s3.UploadPartCopyInput) (s3response.CopyObjectResult, error) {
func (p *Posix) UploadPartCopy(_ context.Context, upi *s3.UploadPartCopyInput) (s3response.CopyObjectResult, error) {
_, err := os.Stat(*upi.Bucket)
if errors.Is(err, fs.ErrNotExist) {
return s3response.CopyObjectResult{}, s3err.GetAPIError(s3err.ErrNoSuchBucket)
@@ -798,7 +882,7 @@ func (p *Posix) UploadPartCopy(upi *s3.UploadPartCopyInput) (s3response.CopyObje
}
if startOffset+length > fi.Size()+1 {
return s3response.CopyObjectResult{}, s3err.GetAPIError(s3err.ErrInvalidRequest)
return s3response.CopyObjectResult{}, s3err.GetAPIError(s3err.ErrInvalidRange)
}
f, err := openTmpFile(filepath.Join(*upi.Bucket, objdir),
@@ -846,7 +930,9 @@ func (p *Posix) UploadPartCopy(upi *s3.UploadPartCopyInput) (s3response.CopyObje
}, nil
}
func (p *Posix) PutObject(po *s3.PutObjectInput) (string, error) {
func (p *Posix) PutObject(ctx context.Context, po *s3.PutObjectInput) (string, error) {
tagsStr := getString(po.Tagging)
tags := make(map[string]string)
_, err := os.Stat(*po.Bucket)
if errors.Is(err, fs.ErrNotExist) {
return "", s3err.GetAPIError(s3err.ErrNoSuchBucket)
@@ -855,6 +941,17 @@ func (p *Posix) PutObject(po *s3.PutObjectInput) (string, error) {
return "", fmt.Errorf("stat bucket: %w", err)
}
if tagsStr != "" {
tagParts := strings.Split(tagsStr, "&")
for _, prt := range tagParts {
p := strings.Split(prt, "=")
if len(p) != 2 {
return "", s3err.GetAPIError(s3err.ErrInvalidTag)
}
tags[p[0]] = p[1]
}
}
name := filepath.Join(*po.Bucket, *po.Key)
if strings.HasSuffix(*po.Key, "/") {
@@ -865,7 +962,7 @@ func (p *Posix) PutObject(po *s3.PutObjectInput) (string, error) {
}
for k, v := range po.Metadata {
xattr.Set(name, "user."+k, []byte(v))
xattr.Set(name, fmt.Sprintf("user.%v.%v", metaHdr, k), []byte(v))
}
// set etag attribute to signify this dir was specifically put
@@ -898,11 +995,18 @@ func (p *Posix) PutObject(po *s3.PutObjectInput) (string, error) {
err = f.link()
if err != nil {
return "", fmt.Errorf("link object in namespace: %w", err)
return "", s3err.GetAPIError(s3err.ErrExistingObjectIsDirectory)
}
for k, v := range po.Metadata {
xattr.Set(name, "user."+k, []byte(v))
xattr.Set(name, fmt.Sprintf("user.%v.%v", metaHdr, k), []byte(v))
}
if tagsStr != "" {
err := p.SetTags(ctx, *po.Bucket, *po.Key, tags)
if err != nil {
return "", err
}
}
dataSum := hash.Sum(nil)
@@ -912,7 +1016,10 @@ func (p *Posix) PutObject(po *s3.PutObjectInput) (string, error) {
return etag, nil
}
func (p *Posix) DeleteObject(bucket, object string) error {
func (p *Posix) DeleteObject(_ context.Context, input *s3.DeleteObjectInput) error {
bucket := *input.Bucket
object := *input.Key
_, err := os.Stat(bucket)
if errors.Is(err, fs.ErrNotExist) {
return s3err.GetAPIError(s3err.ErrNoSuchBucket)
@@ -921,7 +1028,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)
}
@@ -966,19 +1073,47 @@ func (p *Posix) removeParents(bucket, object string) error {
return nil
}
func (p *Posix) DeleteObjects(bucket string, objects *s3.DeleteObjectsInput) error {
func (p *Posix) DeleteObjects(ctx context.Context, input *s3.DeleteObjectsInput) (s3response.DeleteObjectsResult, error) {
// delete object already checks bucket
for _, obj := range objects.Delete.Objects {
err := p.DeleteObject(bucket, *obj.Key)
if err != nil {
return err
delResult, errs := []types.DeletedObject{}, []types.Error{}
for _, obj := range input.Delete.Objects {
//TODO: Make the delete operation concurrent
err := p.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: input.Bucket,
Key: obj.Key,
})
if err == nil {
delResult = append(delResult, types.DeletedObject{Key: obj.Key})
} else {
serr, ok := err.(s3err.APIError)
if ok {
errs = append(errs, types.Error{
Key: obj.Key,
Code: &serr.Code,
Message: &serr.Description,
})
} else {
errs = append(errs, types.Error{
Key: obj.Key,
Code: getStringPtr("InternalError"),
Message: getStringPtr(err.Error()),
})
}
}
}
return nil
return s3response.DeleteObjectsResult{
Deleted: delResult,
Error: errs,
}, nil
}
func (p *Posix) GetObject(bucket, object, acceptRange string, writer io.Writer) (*s3.GetObjectOutput, error) {
func (p *Posix) GetObject(_ context.Context, input *s3.GetObjectInput, writer io.Writer) (*s3.GetObjectOutput, error) {
bucket := *input.Bucket
object := *input.Key
acceptRange := *input.Range
var contentRange string
_, err := os.Stat(bucket)
if errors.Is(err, fs.ErrNotExist) {
return nil, s3err.GetAPIError(s3err.ErrNoSuchBucket)
@@ -1006,7 +1141,11 @@ func (p *Posix) GetObject(bucket, object, acceptRange string, writer io.Writer)
}
if startOffset+length > fi.Size()+1 {
return nil, s3err.GetAPIError(s3err.ErrInvalidRequest)
return nil, s3err.GetAPIError(s3err.ErrInvalidRange)
}
if acceptRange != "" {
contentRange = fmt.Sprintf("bytes %v-%v/%v", startOffset, startOffset+length-1, fi.Size())
}
f, err := os.Open(objPath)
@@ -1048,10 +1187,14 @@ func (p *Posix) GetObject(bucket, object, acceptRange string, writer io.Writer)
LastModified: backend.GetTimePtr(fi.ModTime()),
Metadata: userMetaData,
TagCount: int32(len(tags)),
ContentRange: &contentRange,
}, nil
}
func (p *Posix) HeadObject(bucket, object string) (*s3.HeadObjectOutput, error) {
func (p *Posix) HeadObject(_ context.Context, input *s3.HeadObjectInput) (*s3.HeadObjectOutput, error) {
bucket := *input.Bucket
object := *input.Key
_, err := os.Stat(bucket)
if errors.Is(err, fs.ErrNotExist) {
return nil, s3err.GetAPIError(s3err.ErrNoSuchBucket)
@@ -1088,7 +1231,19 @@ 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(ctx context.Context, input *s3.CopyObjectInput) (*s3.CopyObjectOutput, error) {
srcBucket, srcObject, ok := strings.Cut(*input.CopySource, "/")
if !ok {
return nil, s3err.GetAPIError(s3err.ErrInvalidCopySource)
}
dstBucket := *input.Bucket
dstObject := *input.Key
owner := *input.ExpectedBucketOwner
if fmt.Sprintf("%v/%v", srcBucket, srcObject) == fmt.Sprintf("%v/%v", dstBucket, dstObject) {
return &s3.CopyObjectOutput{}, s3err.GetAPIError(s3err.ErrInvalidCopyDest)
}
_, err := os.Stat(srcBucket)
if errors.Is(err, fs.ErrNotExist) {
return nil, s3err.GetAPIError(s3err.ErrNoSuchBucket)
@@ -1105,6 +1260,22 @@ func (p *Posix) CopyObject(srcBucket, srcObject, dstBucket, dstObject string) (*
return nil, fmt.Errorf("stat bucket: %w", err)
}
dstBucketACLBytes, err := xattr.Get(dstBucket, aclkey)
if err != nil {
return nil, fmt.Errorf("get dst bucket acl tag: %w", err)
}
var dstBucketACL auth.ACL
err = json.Unmarshal(dstBucketACLBytes, &dstBucketACL)
if err != nil {
return nil, fmt.Errorf("parse dst bucket acl: %w", err)
}
err = auth.VerifyACL(dstBucketACL, dstBucket, owner, types.PermissionWrite, false)
if err != nil {
return nil, err
}
objPath := filepath.Join(srcBucket, srcObject)
f, err := os.Open(objPath)
if errors.Is(err, fs.ErrNotExist) {
@@ -1120,7 +1291,7 @@ func (p *Posix) CopyObject(srcBucket, srcObject, dstBucket, dstObject string) (*
return nil, fmt.Errorf("stat object: %w", err)
}
etag, err := p.PutObject(&s3.PutObjectInput{Bucket: &dstBucket, Key: &dstObject, Body: f, ContentLength: fInfo.Size()})
etag, err := p.PutObject(ctx, &s3.PutObjectInput{Bucket: &dstBucket, Key: &dstObject, Body: f, ContentLength: fInfo.Size()})
if err != nil {
return nil, err
}
@@ -1138,7 +1309,13 @@ func (p *Posix) CopyObject(srcBucket, srcObject, dstBucket, dstObject string) (*
}, nil
}
func (p *Posix) ListObjects(bucket, prefix, marker, delim string, maxkeys int) (*s3.ListObjectsOutput, error) {
func (p *Posix) ListObjects(_ context.Context, input *s3.ListObjectsInput) (*s3.ListObjectsOutput, error) {
bucket := *input.Bucket
prefix := *input.Prefix
marker := *input.Marker
delim := *input.Delimiter
maxkeys := input.MaxKeys
_, err := os.Stat(bucket)
if errors.Is(err, fs.ErrNotExist) {
return nil, s3err.GetAPIError(s3err.ErrNoSuchBucket)
@@ -1160,7 +1337,7 @@ func (p *Posix) ListObjects(bucket, prefix, marker, delim string, maxkeys int) (
Delimiter: &delim,
IsTruncated: results.Truncated,
Marker: &marker,
MaxKeys: int32(maxkeys),
MaxKeys: maxkeys,
Name: &bucket,
NextMarker: &results.NextMarker,
Prefix: &prefix,
@@ -1228,7 +1405,13 @@ func fileToObj(bucket string) backend.GetObjFunc {
}
}
func (p *Posix) ListObjectsV2(bucket, prefix, marker, delim string, maxkeys int) (*s3.ListObjectsV2Output, error) {
func (p *Posix) ListObjectsV2(_ context.Context, input *s3.ListObjectsV2Input) (*s3.ListObjectsV2Output, error) {
bucket := *input.Bucket
prefix := *input.Prefix
marker := *input.ContinuationToken
delim := *input.Delimiter
maxkeys := input.MaxKeys
_, err := os.Stat(bucket)
if errors.Is(err, fs.ErrNotExist) {
return nil, s3err.GetAPIError(s3err.ErrNoSuchBucket)
@@ -1238,7 +1421,7 @@ func (p *Posix) ListObjectsV2(bucket, prefix, marker, delim string, maxkeys int)
}
fileSystem := os.DirFS(bucket)
results, err := backend.Walk(fileSystem, prefix, delim, marker, maxkeys,
results, err := backend.Walk(fileSystem, prefix, delim, marker, int32(maxkeys),
fileToObj(bucket), []string{metaTmpDir})
if err != nil {
return nil, fmt.Errorf("walk %v: %w", bucket, err)
@@ -1254,10 +1437,11 @@ func (p *Posix) ListObjectsV2(bucket, prefix, marker, delim string, maxkeys int)
Name: &bucket,
NextContinuationToken: &results.NextMarker,
Prefix: &prefix,
KeyCount: int32(len(results.Objects)),
}, nil
}
func (p *Posix) PutBucketAcl(bucket string, data []byte) error {
func (p *Posix) PutBucketAcl(_ context.Context, bucket string, data []byte) error {
_, err := os.Stat(bucket)
if errors.Is(err, fs.ErrNotExist) {
return s3err.GetAPIError(s3err.ErrNoSuchBucket)
@@ -1273,8 +1457,8 @@ func (p *Posix) PutBucketAcl(bucket string, data []byte) error {
return nil
}
func (p *Posix) GetBucketAcl(bucket string) ([]byte, error) {
_, err := os.Stat(bucket)
func (p *Posix) GetBucketAcl(_ context.Context, input *s3.GetBucketAclInput) ([]byte, error) {
_, err := os.Stat(*input.Bucket)
if errors.Is(err, fs.ErrNotExist) {
return nil, s3err.GetAPIError(s3err.ErrNoSuchBucket)
}
@@ -1282,7 +1466,7 @@ func (p *Posix) GetBucketAcl(bucket string) ([]byte, error) {
return nil, fmt.Errorf("stat bucket: %w", err)
}
b, err := xattr.Get(bucket, aclkey)
b, err := xattr.Get(*input.Bucket, aclkey)
if isNoAttr(err) {
return []byte{}, nil
}
@@ -1292,7 +1476,7 @@ func (p *Posix) GetBucketAcl(bucket string) ([]byte, error) {
return b, nil
}
func (p *Posix) GetTags(bucket, object string) (map[string]string, error) {
func (p *Posix) GetTags(_ context.Context, bucket, object string) (map[string]string, error) {
_, err := os.Stat(bucket)
if errors.Is(err, fs.ErrNotExist) {
return nil, s3err.GetAPIError(s3err.ErrNoSuchBucket)
@@ -1325,7 +1509,7 @@ func (p *Posix) getXattrTags(bucket, object string) (map[string]string, error) {
return tags, nil
}
func (p *Posix) SetTags(bucket, object string, tags map[string]string) error {
func (p *Posix) SetTags(_ context.Context, bucket, object string, tags map[string]string) error {
_, err := os.Stat(bucket)
if errors.Is(err, fs.ErrNotExist) {
return s3err.GetAPIError(s3err.ErrNoSuchBucket)
@@ -1361,8 +1545,83 @@ func (p *Posix) SetTags(bucket, object string, tags map[string]string) error {
return nil
}
func (p *Posix) RemoveTags(bucket, object string) error {
return p.SetTags(bucket, object, nil)
func (p *Posix) RemoveTags(ctx context.Context, bucket, object string) error {
return p.SetTags(ctx, bucket, object, nil)
}
func (p *Posix) ChangeBucketOwner(ctx context.Context, bucket, newOwner string) error {
_, err := os.Stat(bucket)
if errors.Is(err, fs.ErrNotExist) {
return s3err.GetAPIError(s3err.ErrNoSuchBucket)
}
if err != nil {
return fmt.Errorf("stat bucket: %w", err)
}
aclTag, err := xattr.Get(bucket, aclkey)
if err != nil {
return fmt.Errorf("get acl: %w", err)
}
var acl auth.ACL
err = json.Unmarshal(aclTag, &acl)
if err != nil {
return fmt.Errorf("unmarshal acl: %w", err)
}
acl.Owner = newOwner
newAcl, err := json.Marshal(acl)
if err != nil {
return fmt.Errorf("marshal acl: %w", err)
}
err = xattr.Set(bucket, aclkey, newAcl)
if err != nil {
return fmt.Errorf("set acl: %w", err)
}
return nil
}
func (p *Posix) ListBucketsAndOwners(ctx context.Context) (buckets []s3response.Bucket, err error) {
entries, err := os.ReadDir(".")
if err != nil {
return buckets, fmt.Errorf("readdir buckets: %w", err)
}
for _, entry := range entries {
if !entry.IsDir() {
continue
}
fi, err := entry.Info()
if err != nil {
continue
}
aclTag, err := xattr.Get(entry.Name(), aclkey)
if err != nil {
return buckets, fmt.Errorf("get acl tag: %w", err)
}
var acl auth.ACL
err = json.Unmarshal(aclTag, &acl)
if err != nil {
return buckets, fmt.Errorf("parse acl tag: %w", err)
}
buckets = append(buckets, s3response.Bucket{
Name: fi.Name(),
Owner: acl.Owner,
})
}
sort.SliceStable(buckets, func(i, j int) bool {
return buckets[i].Name < buckets[j].Name
})
return buckets, nil
}
const (
@@ -1570,3 +1829,14 @@ func isNoAttr(err error) bool {
}
return false
}
func getString(str *string) string {
if str == nil {
return ""
}
return *str
}
func getStringPtr(str string) *string {
return &str
}

View File

@@ -15,6 +15,7 @@
package scoutfs
import (
"context"
"crypto/sha256"
"encoding/json"
"errors"
@@ -29,7 +30,6 @@ import (
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/pkg/xattr"
"github.com/versity/scoutfs-go"
"github.com/versity/versitygw/backend"
"github.com/versity/versitygw/backend/posix"
"github.com/versity/versitygw/s3err"
@@ -114,7 +114,12 @@ func (*ScoutFS) String() string {
// CompleteMultipartUpload scoutfs complete upload uses scoutfs move blocks
// ioctl to not have to read and copy the part data to the final object. This
// saves a read and write cycle for all mutlipart uploads.
func (s *ScoutFS) CompleteMultipartUpload(bucket, object, uploadID string, parts []types.Part) (*s3.CompleteMultipartUploadOutput, error) {
func (s *ScoutFS) CompleteMultipartUpload(_ context.Context, input *s3.CompleteMultipartUploadInput) (*s3.CompleteMultipartUploadOutput, error) {
bucket := *input.Bucket
object := *input.Key
uploadID := *input.UploadId
parts := input.MultipartUpload.Parts
_, err := os.Stat(bucket)
if errors.Is(err, fs.ErrNotExist) {
return nil, s3err.GetAPIError(s3err.ErrNoSuchBucket)
@@ -160,7 +165,9 @@ func (s *ScoutFS) CompleteMultipartUpload(bucket, object, uploadID string, parts
if err != nil {
etag = ""
}
parts[i].ETag = &etag
if etag != *parts[i].ETag {
return nil, s3err.GetAPIError(s3err.ErrInvalidPart)
}
}
// use totalsize=0 because we wont be writing to the file, only moving
@@ -180,7 +187,7 @@ func (s *ScoutFS) CompleteMultipartUpload(bucket, object, uploadID string, parts
// scoutfs move data is a metadata only operation that moves the data
// extent references from the source, appeding to the destination.
// this needs to be 4k aligned.
err = scoutfs.MoveData(pf, f.f)
err = moveData(pf, f.f)
pf.Close()
if err != nil {
return nil, fmt.Errorf("move blocks part %v: %v", p.PartNumber, err)
@@ -347,7 +354,10 @@ func mkdirAll(path string, perm os.FileMode, bucket, object string) error {
return nil
}
func (s *ScoutFS) HeadObject(bucket, object string) (*s3.HeadObjectOutput, error) {
func (s *ScoutFS) HeadObject(_ context.Context, input *s3.HeadObjectInput) (*s3.HeadObjectOutput, error) {
bucket := *input.Bucket
object := *input.Key
_, err := os.Stat(bucket)
if errors.Is(err, fs.ErrNotExist) {
return nil, s3err.GetAPIError(s3err.ErrNoSuchBucket)
@@ -381,7 +391,7 @@ func (s *ScoutFS) HeadObject(bucket, object string) (*s3.HeadObjectOutput, error
// Check if there are any offline exents associated with this file.
// If so, we will set storage class to glacier.
st, err := scoutfs.StatMore(objPath)
st, err := statMore(objPath)
if errors.Is(err, fs.ErrNotExist) {
return nil, s3err.GetAPIError(s3err.ErrNoSuchKey)
}
@@ -417,7 +427,11 @@ func (s *ScoutFS) HeadObject(bucket, object string) (*s3.HeadObjectOutput, error
}, nil
}
func (s *ScoutFS) GetObject(bucket, object, acceptRange string, writer io.Writer) (*s3.GetObjectOutput, error) {
func (s *ScoutFS) GetObject(_ context.Context, input *s3.GetObjectInput, writer io.Writer) (*s3.GetObjectOutput, error) {
bucket := *input.Bucket
object := *input.Key
acceptRange := *input.Range
_, err := os.Stat(bucket)
if errors.Is(err, fs.ErrNotExist) {
return nil, s3err.GetAPIError(s3err.ErrNoSuchBucket)
@@ -451,7 +465,7 @@ func (s *ScoutFS) GetObject(bucket, object, acceptRange string, writer io.Writer
if s.glaciermode {
// Check if there are any offline exents associated with this file.
// If so, we will return the InvalidObjectState error.
st, err := scoutfs.StatMore(objPath)
st, err := statMore(objPath)
if errors.Is(err, fs.ErrNotExist) {
return nil, s3err.GetAPIError(s3err.ErrNoSuchKey)
}
@@ -527,7 +541,13 @@ func (s *ScoutFS) getXattrTags(bucket, object string) (map[string]string, error)
return tags, nil
}
func (s *ScoutFS) ListObjects(bucket, prefix, marker, delim string, maxkeys int) (*s3.ListObjectsOutput, error) {
func (s *ScoutFS) ListObjects(_ context.Context, input *s3.ListObjectsInput) (*s3.ListObjectsOutput, error) {
bucket := *input.Bucket
prefix := *input.Prefix
marker := *input.Marker
delim := *input.Delimiter
maxkeys := input.MaxKeys
_, err := os.Stat(bucket)
if errors.Is(err, fs.ErrNotExist) {
return nil, s3err.GetAPIError(s3err.ErrNoSuchBucket)
@@ -549,14 +569,20 @@ func (s *ScoutFS) ListObjects(bucket, prefix, marker, delim string, maxkeys int)
Delimiter: &delim,
IsTruncated: results.Truncated,
Marker: &marker,
MaxKeys: int32(maxkeys),
MaxKeys: maxkeys,
Name: &bucket,
NextMarker: &results.NextMarker,
Prefix: &prefix,
}, nil
}
func (s *ScoutFS) ListObjectsV2(bucket, prefix, marker, delim string, maxkeys int) (*s3.ListObjectsV2Output, error) {
func (s *ScoutFS) ListObjectsV2(_ context.Context, input *s3.ListObjectsV2Input) (*s3.ListObjectsV2Output, error) {
bucket := *input.Bucket
prefix := *input.Prefix
marker := *input.ContinuationToken
delim := *input.Delimiter
maxkeys := input.MaxKeys
_, err := os.Stat(bucket)
if errors.Is(err, fs.ErrNotExist) {
return nil, s3err.GetAPIError(s3err.ErrNoSuchBucket)
@@ -566,7 +592,7 @@ func (s *ScoutFS) ListObjectsV2(bucket, prefix, marker, delim string, maxkeys in
}
fileSystem := os.DirFS(bucket)
results, err := backend.Walk(fileSystem, prefix, delim, marker, maxkeys,
results, err := backend.Walk(fileSystem, prefix, delim, marker, int32(maxkeys),
s.fileToObj(bucket), []string{metaTmpDir})
if err != nil {
return nil, fmt.Errorf("walk %v: %w", bucket, err)
@@ -639,7 +665,7 @@ func (s *ScoutFS) fileToObj(bucket string) backend.GetObjFunc {
if s.glaciermode {
// Check if there are any offline exents associated with this file.
// If so, we will return the InvalidObjectState error.
st, err := scoutfs.StatMore(objPath)
st, err := statMore(objPath)
if errors.Is(err, fs.ErrNotExist) {
return types.Object{}, backend.ErrSkipObj
}
@@ -663,7 +689,10 @@ func (s *ScoutFS) fileToObj(bucket string) backend.GetObjFunc {
// RestoreObject will set stage request on file if offline and do nothing if
// file is online
func (s *ScoutFS) RestoreObject(bucket, object string, restoreRequest *s3.RestoreObjectInput) error {
func (s *ScoutFS) RestoreObject(_ context.Context, input *s3.RestoreObjectInput) error {
bucket := *input.Bucket
object := *input.Key
_, err := os.Stat(bucket)
if errors.Is(err, fs.ErrNotExist) {
return s3err.GetAPIError(s3err.ErrNoSuchBucket)

View File

@@ -12,6 +12,8 @@
// specific language governing permissions and limitations
// under the License.
//go:build linux && amd64
package scoutfs
import (
@@ -26,6 +28,7 @@ import (
"golang.org/x/sys/unix"
"github.com/versity/scoutfs-go"
"github.com/versity/versitygw/backend/posix"
)
@@ -182,3 +185,25 @@ func (tmp *tmpfile) Write(b []byte) (int, error) {
func (tmp *tmpfile) cleanup() {
tmp.f.Close()
}
func moveData(from *os.File, to *os.File) error {
return scoutfs.MoveData(from, to)
}
func statMore(path string) (stat, error) {
st, err := scoutfs.StatMore(path)
if err != nil {
return stat{}, err
}
var s stat
s.Meta_seq = st.Meta_seq
s.Data_seq = st.Data_seq
s.Data_version = st.Data_version
s.Online_blocks = st.Online_blocks
s.Offline_blocks = st.Offline_blocks
s.Crtime_sec = st.Crtime_sec
s.Crtime_nsec = st.Crtime_nsec
return s, nil
}

View File

@@ -12,6 +12,8 @@
// specific language governing permissions and limitations
// under the License.
//go:build !(linux && amd64)
package scoutfs
import (
@@ -46,3 +48,11 @@ func (tmp *tmpfile) Write(b []byte) (int, error) {
func (tmp *tmpfile) cleanup() {
}
func moveData(from *os.File, to *os.File) error {
return errNotSupported
}
func statMore(path string) (stat, error) {
return stat{}, errNotSupported
}

View File

@@ -14,35 +14,12 @@
package scoutfs
import (
"errors"
"fmt"
"os"
)
func New(rootdir string, opts ...Option) (*ScoutFS, error) {
return nil, fmt.Errorf("scoutfs only available on linux")
}
type tmpfile struct {
f *os.File
}
var (
errNotSupported = errors.New("not supported")
)
func openTmpFile(dir, bucket, obj string, size int64) (*tmpfile, error) {
return nil, errNotSupported
}
func (tmp *tmpfile) link() error {
return errNotSupported
}
func (tmp *tmpfile) Write(b []byte) (int, error) {
return 0, errNotSupported
}
func (tmp *tmpfile) cleanup() {
type stat struct {
Meta_seq uint64
Data_seq uint64
Data_version uint64
Online_blocks uint64
Offline_blocks uint64
Crtime_sec uint64
Crtime_nsec uint32
}

View File

@@ -38,7 +38,7 @@ var ErrSkipObj = errors.New("skip this object")
// Walk walks the supplied fs.FS and returns results compatible with list
// objects responses
func Walk(fileSystem fs.FS, prefix, delimiter, marker string, max int, getObj GetObjFunc, skipdirs []string) (WalkResults, error) {
func Walk(fileSystem fs.FS, prefix, delimiter, marker string, max int32, getObj GetObjFunc, skipdirs []string) (WalkResults, error) {
cpmap := make(map[string]struct{})
var objects []types.Object
@@ -47,7 +47,7 @@ func Walk(fileSystem fs.FS, prefix, delimiter, marker string, max int, getObj Ge
pastMarker = true
}
var pastMax bool
pastMax := max == 0
var newMarker string
var truncated bool
@@ -55,30 +55,30 @@ func Walk(fileSystem fs.FS, prefix, delimiter, marker string, max int, getObj Ge
if err != nil {
return err
}
// Ignore the root directory
if path == "." {
return nil
}
if contains(d.Name(), skipdirs) {
return fs.SkipDir
}
if pastMax {
newMarker = path
truncated = true
if len(objects) != 0 {
newMarker = *objects[len(objects)-1].Key
truncated = true
}
return fs.SkipAll
}
if d.IsDir() {
// Ignore the root directory
if path == "." {
return nil
}
if contains(d.Name(), skipdirs) {
return fs.SkipDir
}
// If prefix is defined and the directory does not match prefix,
// do not descend into the directory because nothing will
// match this prefix. Make sure to append the / at the end of
// directories since this is implied as a directory path name.
// If path is a prefix of prefix, then path could still be
// building to match. So only skip if path isnt a prefix of prefix
// and prefix isnt a prefix of path.
// building to match. So only skip if path isn't a prefix of prefix
// and prefix isn't a prefix of path.
if prefix != "" &&
!strings.HasPrefix(path+string(os.PathSeparator), prefix) &&
!strings.HasPrefix(prefix, path+string(os.PathSeparator)) {
@@ -106,10 +106,10 @@ func Walk(fileSystem fs.FS, prefix, delimiter, marker string, max int, getObj Ge
}
if !pastMarker {
if path != marker {
return nil
if path == marker {
pastMarker = true
}
pastMarker = true
return nil
}
// If object doesn't have prefix, don't include in results.
@@ -129,7 +129,7 @@ func Walk(fileSystem fs.FS, prefix, delimiter, marker string, max int, getObj Ge
}
objects = append(objects, obj)
if max > 0 && (len(objects)+len(cpmap)) == max {
if max > 0 && (len(objects)+len(cpmap)) == int(max) {
pastMax = true
}
@@ -168,7 +168,7 @@ func Walk(fileSystem fs.FS, prefix, delimiter, marker string, max int, getObj Ge
return fmt.Errorf("file to object %q: %w", path, err)
}
objects = append(objects, obj)
if (len(objects) + len(cpmap)) == max {
if (len(objects) + len(cpmap)) == int(max) {
pastMax = true
}
return nil
@@ -178,7 +178,7 @@ func Walk(fileSystem fs.FS, prefix, delimiter, marker string, max int, getObj Ge
// These are abstractly a "directory", so need to include the
// delimiter at the end.
cpmap[prefix+before+delimiter] = struct{}{}
if (len(objects) + len(cpmap)) == max {
if (len(objects) + len(cpmap)) == int(max) {
pastMax = true
}

View File

@@ -17,30 +17,32 @@ package main
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"text/tabwriter"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
"github.com/urfave/cli/v2"
"github.com/versity/versitygw/auth"
"github.com/versity/versitygw/s3response"
)
var (
adminAccess string
adminSecret string
adminRegion string
adminAccess string
adminSecret string
adminEndpoint string
)
func adminCommand() *cli.Command {
return &cli.Command{
Name: "admin",
Usage: "admin CLI tool",
Description: `admin CLI tool for interacting with admin api.
Here is the available api list:
create-user
`,
Name: "admin",
Usage: "admin CLI tool",
Description: `Admin CLI tool for interacting with admin APIs.`,
Subcommands: []*cli.Command{
{
Name: "create-user",
@@ -49,13 +51,13 @@ func adminCommand() *cli.Command {
Flags: []cli.Flag{
&cli.StringFlag{
Name: "access",
Usage: "access value for the new user",
Usage: "access key id for the new user",
Required: true,
Aliases: []string{"a"},
},
&cli.StringFlag{
Name: "secret",
Usage: "secret value for the new user",
Usage: "secret access key for the new user",
Required: true,
Aliases: []string{"s"},
},
@@ -74,20 +76,50 @@ func adminCommand() *cli.Command {
Flags: []cli.Flag{
&cli.StringFlag{
Name: "access",
Usage: "access value for the user to be deleted",
Usage: "access key id of the user to be deleted",
Required: true,
Aliases: []string{"a"},
},
},
},
{
Name: "list-users",
Usage: "List all the gateway users",
Action: listUsers,
},
{
Name: "change-bucket-owner",
Usage: "Changes the bucket owner",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "bucket",
Usage: "the bucket name to change the owner",
Required: true,
Aliases: []string{"b"},
},
&cli.StringFlag{
Name: "owner",
Usage: "the user access key id, who should be the bucket owner",
Required: true,
Aliases: []string{"o"},
},
},
Action: changeBucketOwner,
},
{
Name: "list-buckets",
Usage: "Lists all the gateway buckets and owners.",
Action: listBuckets,
},
},
Flags: []cli.Flag{
// TODO: create a configuration file for this
&cli.StringFlag{
Name: "access",
Usage: "admin access account",
Usage: "admin access key id",
EnvVars: []string{"ADMIN_ACCESS_KEY_ID", "ADMIN_ACCESS_KEY"},
Aliases: []string{"a"},
Required: true,
Destination: &adminAccess,
},
&cli.StringFlag{
@@ -95,14 +127,16 @@ func adminCommand() *cli.Command {
Usage: "admin secret access key",
EnvVars: []string{"ADMIN_SECRET_ACCESS_KEY", "ADMIN_SECRET_KEY"},
Aliases: []string{"s"},
Required: true,
Destination: &adminSecret,
},
&cli.StringFlag{
Name: "region",
Usage: "s3 region string",
Value: "us-east-1",
Destination: &adminRegion,
Aliases: []string{"r"},
Name: "endpoint-url",
Usage: "admin apis endpoint url",
EnvVars: []string{"ADMIN_ENDPOINT_URL"},
Aliases: []string{"er"},
Required: true,
Destination: &adminEndpoint,
},
},
}
@@ -117,7 +151,7 @@ func createUser(ctx *cli.Context) error {
return fmt.Errorf("invalid input parameter for role")
}
req, err := http.NewRequest(http.MethodPatch, fmt.Sprintf("http://localhost:7070/create-user?access=%v&secret=%v&role=%v", access, secret, role), nil)
req, err := http.NewRequest(http.MethodPatch, fmt.Sprintf("%v/create-user?access=%v&secret=%v&role=%v", adminEndpoint, access, secret, role), nil)
if err != nil {
return fmt.Errorf("failed to send the request: %w", err)
}
@@ -129,7 +163,7 @@ func createUser(ctx *cli.Context) error {
req.Header.Set("X-Amz-Content-Sha256", hexPayload)
signErr := signer.SignHTTP(req.Context(), aws.Credentials{AccessKeyID: adminAccess, SecretAccessKey: adminSecret}, req, hexPayload, "s3", adminRegion, time.Now())
signErr := signer.SignHTTP(req.Context(), aws.Credentials{AccessKeyID: adminAccess, SecretAccessKey: adminSecret}, req, hexPayload, "s3", region, time.Now())
if signErr != nil {
return fmt.Errorf("failed to sign the request: %w", err)
}
@@ -145,8 +179,9 @@ func createUser(ctx *cli.Context) error {
if err != nil {
return err
}
defer resp.Body.Close()
fmt.Printf("%s", body)
fmt.Printf("%s\n", body)
return nil
}
@@ -157,7 +192,7 @@ func deleteUser(ctx *cli.Context) error {
return fmt.Errorf("invalid input parameter for the new user")
}
req, err := http.NewRequest(http.MethodPatch, fmt.Sprintf("http://localhost:7070/delete-user?access=%v", access), nil)
req, err := http.NewRequest(http.MethodPatch, fmt.Sprintf("%v/delete-user?access=%v", adminEndpoint, access), nil)
if err != nil {
return fmt.Errorf("failed to send the request: %w", err)
}
@@ -169,7 +204,7 @@ func deleteUser(ctx *cli.Context) error {
req.Header.Set("X-Amz-Content-Sha256", hexPayload)
signErr := signer.SignHTTP(req.Context(), aws.Credentials{AccessKeyID: adminAccess, SecretAccessKey: adminSecret}, req, hexPayload, "s3", adminRegion, time.Now())
signErr := signer.SignHTTP(req.Context(), aws.Credentials{AccessKeyID: adminAccess, SecretAccessKey: adminSecret}, req, hexPayload, "s3", region, time.Now())
if signErr != nil {
return fmt.Errorf("failed to sign the request: %w", err)
}
@@ -185,8 +220,165 @@ func deleteUser(ctx *cli.Context) error {
if err != nil {
return err
}
defer resp.Body.Close()
fmt.Printf("%s", body)
fmt.Printf("%s\n", body)
return nil
}
func listUsers(ctx *cli.Context) error {
req, err := http.NewRequest(http.MethodPatch, fmt.Sprintf("%v/list-users", adminEndpoint), nil)
if err != nil {
return fmt.Errorf("failed to send the request: %w", err)
}
signer := v4.NewSigner()
hashedPayload := sha256.Sum256([]byte{})
hexPayload := hex.EncodeToString(hashedPayload[:])
req.Header.Set("X-Amz-Content-Sha256", hexPayload)
signErr := signer.SignHTTP(req.Context(), aws.Credentials{AccessKeyID: adminAccess, SecretAccessKey: adminSecret}, req, hexPayload, "s3", region, time.Now())
if signErr != nil {
return fmt.Errorf("failed to sign the request: %w", err)
}
client := http.Client{}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("failed to send the request: %w", err)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
defer resp.Body.Close()
var accs []auth.Account
if err := json.Unmarshal(body, &accs); err != nil {
return err
}
printAcctTable(accs)
return nil
}
const (
// account table formatting
minwidth int = 2 // minimal cell width including any padding
tabwidth int = 0 // width of tab characters (equivalent number of spaces)
padding int = 2 // padding added to a cell before computing its width
padchar byte = ' ' // ASCII char used for padding
flags uint = 0 // formatting control flags
)
func printAcctTable(accs []auth.Account) {
w := new(tabwriter.Writer)
w.Init(os.Stdout, minwidth, tabwidth, padding, padchar, flags)
fmt.Fprintln(w, "Account\tRole")
fmt.Fprintln(w, "-------\t----")
for _, acc := range accs {
fmt.Fprintf(w, "%v\t%v\n", acc.Access, acc.Role)
}
fmt.Fprintln(w)
w.Flush()
}
func changeBucketOwner(ctx *cli.Context) error {
bucket, owner := ctx.String("bucket"), ctx.String("owner")
req, err := http.NewRequest(http.MethodPatch, fmt.Sprintf("%v/change-bucket-owner/?bucket=%v&owner=%v", adminEndpoint, bucket, owner), nil)
if err != nil {
return fmt.Errorf("failed to send the request: %w", err)
}
signer := v4.NewSigner()
hashedPayload := sha256.Sum256([]byte{})
hexPayload := hex.EncodeToString(hashedPayload[:])
req.Header.Set("X-Amz-Content-Sha256", hexPayload)
signErr := signer.SignHTTP(req.Context(), aws.Credentials{AccessKeyID: adminAccess, SecretAccessKey: adminSecret}, req, hexPayload, "s3", region, time.Now())
if signErr != nil {
return fmt.Errorf("failed to sign the request: %w", err)
}
client := http.Client{}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("failed to send the request: %w", err)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
defer resp.Body.Close()
fmt.Println(string(body))
return nil
}
func printBuckets(buckets []s3response.Bucket) {
w := new(tabwriter.Writer)
w.Init(os.Stdout, minwidth, tabwidth, padding, padchar, flags)
fmt.Fprintln(w, "Bucket\tOwner")
fmt.Fprintln(w, "-------\t----")
for _, acc := range buckets {
fmt.Fprintf(w, "%v\t%v\n", acc.Name, acc.Owner)
}
fmt.Fprintln(w)
w.Flush()
}
func listBuckets(ctx *cli.Context) error {
req, err := http.NewRequest(http.MethodPatch, fmt.Sprintf("%v/list-buckets", adminEndpoint), nil)
if err != nil {
return fmt.Errorf("failed to send the request: %w", err)
}
signer := v4.NewSigner()
hashedPayload := sha256.Sum256([]byte{})
hexPayload := hex.EncodeToString(hashedPayload[:])
req.Header.Set("X-Amz-Content-Sha256", hexPayload)
signErr := signer.SignHTTP(req.Context(), aws.Credentials{AccessKeyID: adminAccess, SecretAccessKey: adminSecret}, req, hexPayload, "s3", region, time.Now())
if signErr != nil {
return fmt.Errorf("failed to sign the request: %w", err)
}
client := http.Client{}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("failed to send the request: %w", err)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return fmt.Errorf(string(body))
}
var buckets []s3response.Bucket
if err := json.Unmarshal(body, &buckets); err != nil {
return err
}
printBuckets(buckets)
return nil
}

View File

@@ -20,6 +20,7 @@ import (
"fmt"
"log"
"os"
"strconv"
"github.com/gofiber/fiber/v2"
"github.com/urfave/cli/v2"
@@ -27,15 +28,22 @@ import (
"github.com/versity/versitygw/backend"
"github.com/versity/versitygw/s3api"
"github.com/versity/versitygw/s3api/middlewares"
"github.com/versity/versitygw/s3event"
"github.com/versity/versitygw/s3log"
)
var (
port string
rootUserAccess string
rootUserSecret string
region string
certFile, keyFile string
debug bool
port, admPort string
rootUserAccess string
rootUserSecret string
region string
admCertFile, admKeyFile string
certFile, keyFile string
kafkaURL, kafkaTopic, kafkaKey string
natsURL, natsTopic string
logWebhookURL string
accessLog string
debug bool
)
var (
@@ -136,19 +144,84 @@ func initFlags() []cli.Flag {
Usage: "TLS key file",
Destination: &keyFile,
},
&cli.StringFlag{
Name: "admin-port",
Usage: "gateway admin server listen address <ip>:<port> or :<port>",
Destination: &admPort,
Aliases: []string{"ap"},
},
&cli.StringFlag{
Name: "admin-cert",
Usage: "TLS cert file for admin server",
Destination: &admCertFile,
},
&cli.StringFlag{
Name: "admin-cert-key",
Usage: "TLS key file for admin server",
Destination: &admKeyFile,
},
&cli.BoolFlag{
Name: "debug",
Usage: "enable debug output",
Destination: &debug,
},
&cli.StringFlag{
Name: "access-log",
Usage: "enable server access logging to specified file",
EnvVars: []string{"LOGFILE"},
Destination: &accessLog,
},
&cli.StringFlag{
Name: "log-webhook-url",
Usage: "webhook url to send the audit logs",
EnvVars: []string{"WEBHOOK"},
Destination: &logWebhookURL,
},
&cli.StringFlag{
Name: "event-kafka-url",
Usage: "kafka server url to send the bucket notifications.",
Destination: &kafkaURL,
Aliases: []string{"eku"},
},
&cli.StringFlag{
Name: "event-kafka-topic",
Usage: "kafka server pub-sub topic to send the bucket notifications to",
Destination: &kafkaTopic,
Aliases: []string{"ekt"},
},
&cli.StringFlag{
Name: "event-kafka-key",
Usage: "kafka server put-sub topic key to send the bucket notifications to",
Destination: &kafkaKey,
Aliases: []string{"ekk"},
},
&cli.StringFlag{
Name: "event-nats-url",
Usage: "nats server url to send the bucket notifications",
Destination: &natsURL,
Aliases: []string{"enu"},
},
&cli.StringFlag{
Name: "event-nats-topic",
Usage: "nats server pub-sub topic to send the bucket notifications to",
Destination: &natsTopic,
Aliases: []string{"ent"},
},
}
}
func runGateway(ctx *cli.Context, be backend.Backend, s auth.Storer) error {
// int32 max for 32 bit arch
blimit := int64(2*1024*1024*1024 - 1)
if strconv.IntSize > 32 {
// 5GB max for 64 bit arch
blimit = int64(5 * 1024 * 1024 * 1024)
}
app := fiber.New(fiber.Config{
AppName: "versitygw",
ServerHeader: "VERSITYGW",
BodyLimit: 5 * 1024 * 1024 * 1024,
BodyLimit: int(blimit),
})
var opts []s3api.Option
@@ -167,10 +240,34 @@ func runGateway(ctx *cli.Context, be backend.Backend, s auth.Storer) error {
}
opts = append(opts, s3api.WithTLS(cert))
}
if debug {
opts = append(opts, s3api.WithDebug())
}
if admPort == "" {
opts = append(opts, s3api.WithAdminServer())
}
admApp := fiber.New(fiber.Config{
AppName: "versitygw",
ServerHeader: "VERSITYGW",
})
var admOpts []s3api.AdminOpt
if admCertFile != "" || admKeyFile != "" {
if admCertFile == "" {
return fmt.Errorf("TLS key specified without cert file")
}
if admKeyFile == "" {
return fmt.Errorf("TLS cert specified without key file")
}
cert, err := tls.LoadX509KeyPair(admCertFile, admKeyFile)
if err != nil {
return fmt.Errorf("tls: load certs: %v", err)
}
admOpts = append(admOpts, s3api.WithAdminSrvTLS(cert))
}
err := s.InitIAM()
if err != nil {
@@ -182,23 +279,67 @@ func runGateway(ctx *cli.Context, be backend.Backend, s auth.Storer) error {
return fmt.Errorf("setup internal iam service: %w", err)
}
logger, err := s3log.InitLogger(&s3log.LogConfig{
LogFile: accessLog,
WebhookURL: logWebhookURL,
})
if err != nil {
return fmt.Errorf("setup logger: %w", err)
}
evSender, err := s3event.InitEventSender(&s3event.EventConfig{
KafkaURL: kafkaURL,
KafkaTopic: kafkaTopic,
KafkaTopicKey: kafkaKey,
NatsURL: natsURL,
NatsTopic: natsTopic,
})
if err != nil {
return fmt.Errorf("unable to connect to the message broker: %w", err)
}
srv, err := s3api.New(app, be, middlewares.RootUserConfig{
Access: rootUserAccess,
Secret: rootUserSecret,
}, port, region, iam, opts...)
}, port, region, iam, logger, evSender, opts...)
if err != nil {
return fmt.Errorf("init gateway: %v", err)
}
c := make(chan error, 1)
go func() { c <- srv.Serve() }()
admSrv := s3api.NewAdminServer(admApp, be, middlewares.RootUserConfig{Access: rootUserAccess, Secret: rootUserSecret}, admPort, region, iam, admOpts...)
select {
case <-ctx.Done():
be.Shutdown()
return ctx.Err()
case err := <-c:
be.Shutdown()
return err
c := make(chan error, 2)
go func() { c <- srv.Serve() }()
if admPort != "" {
go func() { c <- admSrv.Serve() }()
}
// for/select blocks until shutdown
Loop:
for {
select {
case <-ctx.Done():
err = ctx.Err()
break Loop
case err = <-c:
break Loop
case <-sigHup:
if logger != nil {
err = logger.HangUp()
if err != nil {
err = fmt.Errorf("HUP logger: %w", err)
break Loop
}
}
}
}
be.Shutdown()
if logger != nil {
lerr := logger.Shutdown()
if lerr != nil {
fmt.Fprintf(os.Stderr, "shutdown logger: %v\n", lerr)
}
}
return err
}

View File

@@ -23,6 +23,7 @@ import (
var (
sigDone = make(chan bool, 1)
sigHup = make(chan bool, 1)
)
func setupSignalHandler() {
@@ -36,6 +37,7 @@ func setupSignalHandler() {
case syscall.SIGINT, syscall.SIGTERM:
sigDone <- true
case syscall.SIGHUP:
sigHup <- true
}
}
}()

View File

@@ -67,114 +67,6 @@ func initTestFlags() []cli.Flag {
func initTestCommands() []*cli.Command {
return []*cli.Command{
{
Name: "bucket-actions",
Usage: "Test bucket creation, checking the existence, deletes it.",
Description: `Calls s3 gateway create-bucket action to create a new bucket,
calls head-bucket action to check the existence, then calls delete-bucket action to delete the bucket.`,
Action: getAction(integration.TestMakeBucket),
},
{
Name: "object-actions",
Usage: "Test put/get/delete/copy objects.",
Description: `Creates a bucket with s3 gateway action, puts an object in it,
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),
},
{
Name: "put-get-mp-object",
Usage: "Test put & get multipart object.",
Description: `Creates a bucket with s3 gateway action, puts an object in it with multipart upload,
gets the object from the bucket, deletes both the object and bucket.`,
Action: getAction(integration.TestPutGetMPObject),
},
{
Name: "put-dir-object",
Usage: "Test put directory object.",
Description: `Creates a bucket with s3 gateway action, puts a directory object in it,
lists the bucket's objects, deletes both the objects and bucket.`,
Action: getAction(integration.TestPutDirObject),
},
{
Name: "list-objects",
Usage: "Test list-objects action.",
Description: `Creates a bucket with s3 gateway action, puts 2 directory objects in it,
lists the bucket's objects, deletes both the objects and bucket.`,
Action: getAction(integration.TestListObject),
},
{
Name: "abort-mp",
Usage: "Tests abort-multipart-upload action.",
Description: `Creates a bucket with s3 gateway action, creates a multipart upload,
lists the multipart upload, aborts the multipart upload, lists the multipart upload again,
deletes both the objects and bucket.`,
Action: getAction(integration.TestListAbortMultiPartObject),
},
{
Name: "list-parts",
Usage: "Tests list-parts action.",
Description: `Creates a bucket with s3 gateway action, creates a multipart upload,
lists the upload parts, deletes both the objects and bucket.`,
Action: getAction(integration.TestListMultiParts),
},
{
Name: "incorrect-mp",
Usage: "Tests incorrect multipart case.",
Description: `Creates a bucket with s3 gateway action, creates a multipart upload,
uploads different parts, completes the multipart upload with incorrect part numbers,
calls the head-object action, compares the content length, removes both the object and bucket`,
Action: getAction(integration.TestIncorrectMultiParts),
},
{
Name: "incomplete-mp",
Usage: "Tests incomplete multi parts.",
Description: `Creates a bucket with s3 gateway action, creates a multipart upload,
upload a part, lists the parts, checks if the uploaded part is in the list,
removes both the object and the bucket`,
Action: getAction(integration.TestIncompleteMultiParts),
},
{
Name: "incomplete-put-object",
Usage: "Tests incomplete put objects case.",
Description: `Creates a bucket with s3 gateway action, puts an object in it,
gets the object with head-object action, expects the object to be got,
removes both the object and bucket`,
Action: getAction(integration.TestIncompletePutObject),
},
{
Name: "get-range",
Usage: "Tests get object by range.",
Description: `Creates a bucket with s3 gateway action, puts an object in it,
gets the object by specifying the object range, compares the range with the original one,
removes both the object and the bucket`,
Action: getAction(integration.TestRangeGet),
},
{
Name: "invalid-mp",
Usage: "Tests invalid multi part case.",
Description: `Creates a bucket with s3 gateway action, creates a multi part upload,
uploads an invalid part, gets the object with head-object action, expects to get error,
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.",

66
go.mod
View File

@@ -3,51 +3,57 @@ module github.com/versity/versitygw
go 1.20
require (
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/smithy-go v1.13.5
github.com/gofiber/fiber/v2 v2.46.0
github.com/google/uuid v1.3.0
github.com/aws/aws-sdk-go-v2 v1.21.0
github.com/aws/aws-sdk-go-v2/service/s3 v1.38.5
github.com/aws/smithy-go v1.14.2
github.com/gofiber/fiber/v2 v2.49.2
github.com/google/uuid v1.3.1
github.com/nats-io/nats.go v1.29.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/segmentio/kafka-go v0.4.42
github.com/urfave/cli/v2 v2.25.7
github.com/valyala/fasthttp v1.50.0
github.com/versity/scoutfs-go v0.0.0-20230606232754-0474b14343b9
golang.org/x/sys v0.9.0
golang.org/x/sys v0.12.0
)
require (
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.4 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.35 // indirect
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/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.11 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.42 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.14.0 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.16.0 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.22.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/nats-io/nats-server/v2 v2.9.20 // indirect
github.com/nats-io/nkeys v0.4.4 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/pierrec/lz4/v4 v4.1.18 // indirect
github.com/stretchr/testify v1.8.1 // indirect
golang.org/x/crypto v0.11.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
)
require (
github.com/andybalholm/brotli v1.0.5 // indirect
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/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
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.29 // indirect
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/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.13 // indirect
github.com/aws/aws-sdk-go-v2/config v1.18.40
github.com/aws/aws-sdk-go-v2/credentials v1.13.38
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.84
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.41 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.35 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.1.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.14 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.36 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.35 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.15.4 // 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
github.com/philhofer/fwd v1.1.2 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 // indirect
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
github.com/tinylib/msgp v1.1.8 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect

191
go.sum
View File

@@ -1,144 +1,167 @@
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=
github.com/aws/aws-sdk-go-v2 v1.18.1/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 h1:dK82zF6kkPeCo8J1e+tGx4JdvDIQzj7ygIoLg8WMuGs=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10/go.mod h1:VeTZetY5KRJLuD/7fkQXMU6Mw7H5m/KP2J5Iy9osMno=
github.com/aws/aws-sdk-go-v2/config v1.18.27 h1:Az9uLwmssTE6OGTpsFqOnaGpLnKDqNYOJzWuC6UAYzA=
github.com/aws/aws-sdk-go-v2/config v1.18.27/go.mod h1:0My+YgmkGxeqjXZb5BYme5pc4drjTnM+x1GJ3zv42Nw=
github.com/aws/aws-sdk-go-v2/credentials v1.13.26 h1:qmU+yhKmOCyujmuPY7tf5MxR/RKyZrOPO3V4DobiTUk=
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/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=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.28/go.mod h1:7VRpKQQedkfIEXb4k52I7swUnZP0wohVajJMRn3vsUw=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.35 h1:LWA+3kDM8ly001vJ1X1waCuLJdtTl48gwkPKWy9sosI=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.35/go.mod h1:0Eg1YjxE0Bhn56lx+SHJwCzhW+2JGtizsrx+lCqrfm0=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.26 h1:wscW+pnn3J1OYnanMnza5ZVYXLX4cKk5rAvUAl4Qu+c=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.26/go.mod h1:MtYiox5gvyB+OyP0Mr0Sm/yzbEAIPL9eijj/ouHAPw0=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 h1:y2+VQzC6Zh2ojtV2LoC0MNwHWc6qXv/j2vrQtlftkdA=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11/go.mod h1:iV4q2hsqtNECrfmlXyord9u4zyuFEJX9eLgLpSPzWA8=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.29 h1:zZSLP3v3riMOP14H7b4XP0uyfREDQOYv2cqIrvTXDNQ=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.29/go.mod h1:z7EjRjVwZ6pWcWdI2H64dKttvzaP99jRIj5hphW0M5U=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.28 h1:bkRyG4a929RCnpVSTvLM2j/T4ls015ZhhYApbmYs15s=
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/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=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.12/go.mod h1:E4VrHCPzmVB/KFXtqBGKb3c8zpbNBgKe3fisDNLAW5w=
github.com/aws/aws-sdk-go-v2/service/sts v1.19.2 h1:XFJ2Z6sNUUcAz9poj+245DMkrHE4h2j5I9/xD50RHfE=
github.com/aws/aws-sdk-go-v2/service/sts v1.19.2/go.mod h1:dp0yLPsLBOi++WTxzCjA/oZqi6NPIhoR+uF7GeMU9eg=
github.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8=
github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
github.com/aws/aws-sdk-go-v2 v1.21.0 h1:gMT0IW+03wtYJhRqTVYn0wLzwdnK9sRMcxmtfGzRdJc=
github.com/aws/aws-sdk-go-v2 v1.21.0/go.mod h1:/RfNgGmRxI+iFOB1OeJUyxiU+9s88k3pfHvDagGEp0M=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.13 h1:OPLEkmhXf6xFPiz0bLeDArZIDx1NNS4oJyG4nv3Gct0=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.13/go.mod h1:gpAbvyDGQFozTEmlTFO8XcQKHzubdq0LzRyJpG6MiXM=
github.com/aws/aws-sdk-go-v2/config v1.18.40 h1:dbu1llI/nTIL+r6sYHMeVLl99DM8J8/o1I4EPurnhLg=
github.com/aws/aws-sdk-go-v2/config v1.18.40/go.mod h1:JjrCZQwSPGCoZRQzKHyZNNueaKO+kFaEy2sR6mCzd90=
github.com/aws/aws-sdk-go-v2/credentials v1.13.38 h1:gDAuCdVlA4lmmgQhvpZlscwicloCqH44vkxLklGkQLA=
github.com/aws/aws-sdk-go-v2/credentials v1.13.38/go.mod h1:sD4G/Ybgp6s89mWIES3Xn97CsRLpxvz9uVSdv0UxY8I=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.11 h1:uDZJF1hu0EVT/4bogChk8DyjSF6fof6uL/0Y26Ma7Fg=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.11/go.mod h1:TEPP4tENqBGO99KwVpV9MlOX4NSrSLP8u3KRy2CDwA8=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.84 h1:LENrVcqnWTyI8fbIUCvxAMe+fXbREIaXzcR8WPwco1U=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.84/go.mod h1:LHxCiYAStsgps4srke7HujyADd504MSkNXjLpOtICTc=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.41 h1:22dGT7PneFMx4+b3pz7lMTRyN8ZKH7M2cW4GP9yUS2g=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.41/go.mod h1:CrObHAuPneJBlfEJ5T3szXOUkLEThaGfvnhTf33buas=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.35 h1:SijA0mgjV8E+8G45ltVHs0fvKpTj8xmZJ3VwhGKtUSI=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.35/go.mod h1:SJC1nEVVva1g3pHAIdCp7QsRIkMmLAgoDquQ9Rr8kYw=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.42 h1:GPUcE/Yq7Ur8YSUk6lVkoIMWnJNO0HT18GUzCWCgCI0=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.42/go.mod h1:rzfdUlfA+jdgLDmPKjd3Chq9V7LVLYo1Nz++Wb91aRo=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.1.4 h1:6lJvvkQ9HmbHZ4h/IEwclwv2mrTW8Uq1SOB/kXy0mfw=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.1.4/go.mod h1:1PrKYwxTM+zjpw9Y41KFtoJCQrJ34Z47Y4VgVbfndjo=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.14 h1:m0QTSI6pZYJTk5WSKx3fm5cNW/DCicVzULBgU/6IyD0=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.14/go.mod h1:dDilntgHy9WnHXsh7dDtUPgHKEfTJIBUTHM8OWm0f/0=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.36 h1:eev2yZX7esGRjqRbnVk1UxMLw4CyVZDpZXRCcy75oQk=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.36/go.mod h1:lGnOkH9NJATw0XEPcAknFBj3zzNTEGRHtSw+CwC1YTg=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.35 h1:CdzPW9kKitgIiLV1+MHobfR5Xg25iYnyzWZhyQuSlDI=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.35/go.mod h1:QGF2Rs33W5MaN9gYdEQOBBFPLwTZkEhRwI33f7KIG0o=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.15.4 h1:v0jkRigbSD6uOdwcaUQmgEwG1BkPfAPDqaeNt/29ghg=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.15.4/go.mod h1:LhTyt8J04LL+9cIt7pYJ5lbS/U98ZmXovLOR/4LUsk8=
github.com/aws/aws-sdk-go-v2/service/s3 v1.38.5 h1:A42xdtStObqy7NGvzZKpnyNXvoOmm+FENobZ0/ssHWk=
github.com/aws/aws-sdk-go-v2/service/s3 v1.38.5/go.mod h1:rDGMZA7f4pbmTtPOk5v5UM2lmX6UAbRnMDJeDvnH7AM=
github.com/aws/aws-sdk-go-v2/service/sso v1.14.0 h1:AR/hlTsCyk1CwlyKnPFvIMvnONydRjDDRT9OGb0i+/g=
github.com/aws/aws-sdk-go-v2/service/sso v1.14.0/go.mod h1:fIAwKQKBFu90pBxx07BFOMJLpRUGu8VOzLJakeY+0K4=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.16.0 h1:vbgiXuhtn49+erlPrgIvQ+J32rg1HseaPf8lEpKbkxQ=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.16.0/go.mod h1:yygr8ACQRY2PrEcy3xsUI357stq2AxnFM6DIsR9lij4=
github.com/aws/aws-sdk-go-v2/service/sts v1.22.0 h1:s4bioTgjSFRwOoyEFzAVCmFmoowBgjTR8gkrF/sQ4wk=
github.com/aws/aws-sdk-go-v2/service/sts v1.22.0/go.mod h1:VC7JDqsqiwXukYEDjoHh9U0fOJtNWh04FPQz4ct4GGU=
github.com/aws/smithy-go v1.14.2 h1:MJU9hqBGbvWZdApzpvoF2WAIJDbtjK2NDJSiJP7HblQ=
github.com/aws/smithy-go v1.14.2/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
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/gofiber/fiber/v2 v2.49.2 h1:ONEN3/Vc+dUCxxDgZZwpqvhISgHqb+bu+isBiEyKEQs=
github.com/gofiber/fiber/v2 v2.49.2/go.mod h1:gNsKnyrmfEWFpJxQAV0qvW6l70K1dZGno12oLtukcts=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
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/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
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=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
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/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g=
github.com/nats-io/jwt/v2 v2.4.1 h1:Y35W1dgbbz2SQUYDPCaclXcuqleVmpbRa7646Jf2EX4=
github.com/nats-io/nats-server/v2 v2.9.20 h1:bt1dW6xsL1hWWwv7Hovm+EJt5L6iplyqlgEFkoEUk0k=
github.com/nats-io/nats-server/v2 v2.9.20/go.mod h1:aTb/xtLCGKhfTFLxP591CMWfkdgBmcUUSkiSOe5A3gw=
github.com/nats-io/nats.go v1.29.0 h1:dSXZ+SZeGyTdHVYeXimeq12FsIpb9dM8CJ2IZFiHcyE=
github.com/nats-io/nats.go v1.29.0/go.mod h1:XpbWUlOElGwTYbMR7imivs7jJj9GtK7ypv321Wp6pjc=
github.com/nats-io/nkeys v0.4.4 h1:xvBJ8d69TznjcQl9t6//Q5xXuVhyYiSos6RPtvQNTwA=
github.com/nats-io/nkeys v0.4.4/go.mod h1:XUkxdLPTufzlihbamfzQ7mw/VGx6ObUs+0bN5sNvt64=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ=
github.com/pierrec/lz4/v4 v4.1.18/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/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=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 h1:rmMl4fXJhKMNWl+K+r/fq4FbbKI+Ia2m9hYBLm2h4G4=
github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94/go.mod h1:90zrgN3D/WJsDd1iXHT96alCoN2KJo6/4x1DZC3wZs8=
github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d/go.mod h1:Gy+0tqhJvgGlqnTF8CVGP0AaGRjwBtXs/a5PA0Y3+A4=
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/segmentio/kafka-go v0.4.42 h1:qffhBZCz4WcWyNuHEclHjIMLs2slp6mZO8px+5W5tfU=
github.com/segmentio/kafka-go v0.4.42/go.mod h1:d0g15xPMqoUookug0OU75DhGZxXwCFxSLeJ4uphwJzg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
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/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
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/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
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.50.0 h1:H7fweIlBm0rXLs2q0XbalvJ6r0CUPFWK3/bB4N13e9M=
github.com/valyala/fasthttp v1.50.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=
github.com/versity/scoutfs-go v0.0.0-20230606232754-0474b14343b9/go.mod h1:gJsq73k+4685y+rbDIpPY8i/5GbsiwP6JFoFyUDB1fQ=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
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/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
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-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-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
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/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-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=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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.5.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.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.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/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
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=

210
integration/action-tests.go Normal file
View File

@@ -0,0 +1,210 @@
package integration
func TestAuthentication(s *S3Conf) {
Authentication_empty_auth_header(s)
Authentication_invalid_auth_header(s)
Authentication_unsupported_signature_version(s)
Authentication_malformed_credentials(s)
Authentication_malformed_credentials_invalid_parts(s)
Authentication_credentials_terminated_string(s)
Authentication_credentials_incorrect_service(s)
Authentication_credentials_incorrect_region(s)
Authentication_credentials_invalid_date(s)
Authentication_credentials_future_date(s)
Authentication_credentials_past_date(s)
Authentication_credentials_non_existing_access_key(s)
Authentication_invalid_signed_headers(s)
Authentication_missing_date_header(s)
Authentication_invalid_date_header(s)
Authentication_date_mismatch(s)
Authentication_incorrect_payload_hash(s)
Authentication_incorrect_md5(s)
Authentication_signature_error_incorrect_secret_key(s)
}
func TestCreateBucket(s *S3Conf) {
CreateBucket_invalid_bucket_name(s)
CreateBucket_existing_bucket(s)
CreateBucket_as_user(s)
CreateDeleteBucket_success(s)
}
func TestHeadBucket(s *S3Conf) {
HeadBucket_non_existing_bucket(s)
HeadBucket_success(s)
}
func TestListBuckets(s *S3Conf) {
ListBuckets_as_user(s)
ListBuckets_as_admin(s)
ListBuckets_success(s)
}
func TestDeleteBucket(s *S3Conf) {
DeleteBucket_non_existing_bucket(s)
DeleteBucket_non_empty_bucket(s)
}
func TestPutObject(s *S3Conf) {
PutObject_non_existing_bucket(s)
PutObject_special_chars(s)
PutObject_existing_dir_obj(s)
PutObject_obj_parent_is_file(s)
PutObject_success(s)
}
func TestHeadObject(s *S3Conf) {
HeadObject_non_existing_object(s)
HeadObject_success(s)
}
func TestGetObject(s *S3Conf) {
GetObject_non_existing_key(s)
GetObject_invalid_ranges(s)
GetObject_with_meta(s)
GetObject_success(s)
GetObject_by_range_success(s)
}
func TestListObjects(s *S3Conf) {
ListObjects_non_existing_bucket(s)
ListObjects_with_prefix(s)
ListObject_truncated(s)
ListObjects_invalid_max_keys(s)
ListObjects_max_keys_0(s)
ListObjects_delimiter(s)
}
func TestDeleteObject(s *S3Conf) {
DeleteObject_non_existing_object(s)
DeleteObject_success(s)
}
func TestDeleteObjects(s *S3Conf) {
DeleteObjects_empty_input(s)
DeleteObjects_non_existing_objects(s)
DeleteObjects_success(s)
}
func TestCopyObject(s *S3Conf) {
CopyObject_non_existing_dst_bucket(s)
CopyObject_not_owned_source_bucket(s)
CopyObject_copy_to_itself(s)
CopyObject_success(s)
}
func TestPutObjectTagging(s *S3Conf) {
PutObjectTagging_non_existing_object(s)
PutObjectTagging_success(s)
}
func TestGetObjectTagging(s *S3Conf) {
GetObjectTagging_non_existing_object(s)
GetObjectTagging_success(s)
}
func TestDeleteObjectTagging(s *S3Conf) {
DeleteObjectTagging_non_existing_object(s)
DeleteObjectTagging_success(s)
}
func TestCreateMultipartUpload(s *S3Conf) {
CreateMultipartUpload_non_existing_bucket(s)
CreateMultipartUpload_success(s)
}
func TestUploadPart(s *S3Conf) {
UploadPart_non_existing_bucket(s)
UploadPart_invalid_part_number(s)
UploadPart_non_existing_key(s)
UploadPart_non_existing_mp_upload(s)
UploadPart_success(s)
}
func TestUploadPartCopy(s *S3Conf) {
UploadPartCopy_non_existing_bucket(s)
UploadPartCopy_incorrect_uploadId(s)
UploadPartCopy_incorrect_object_key(s)
UploadPartCopy_invalid_part_number(s)
UploadPartCopy_invalid_copy_source(s)
UploadPartCopy_non_existing_source_bucket(s)
UploadPartCopy_non_existing_source_object_key(s)
UploadPartCopy_success(s)
UploadPartCopy_by_range_invalid_range(s)
UploadPartCopy_greater_range_than_obj_size(s)
UploadPartCopy_by_range_success(s)
}
func TestListParts(s *S3Conf) {
ListParts_incorrect_uploadId(s)
ListParts_incorrect_object_key(s)
ListParts_success(s)
}
func TestListMultipartUploads(s *S3Conf) {
ListMultipartUploads_non_existing_bucket(s)
ListMultipartUploads_empty_result(s)
ListMultipartUploads_invalid_max_uploads(s)
ListMultipartUploads_max_uploads(s)
ListMultipartUploads_incorrect_next_key_marker(s)
ListMultipartUploads_ignore_upload_id_marker(s)
ListMultipartUploads_success(s)
}
func TestAbortMultipartUpload(s *S3Conf) {
AbortMultipartUpload_non_existing_bucket(s)
AbortMultipartUpload_incorrect_uploadId(s)
AbortMultipartUpload_incorrect_object_key(s)
AbortMultipartUpload_success(s)
}
func TestCompleteMultipartUpload(s *S3Conf) {
CompletedMultipartUpload_non_existing_bucket(s)
CompleteMultipartUpload_invalid_part_number(s)
CompleteMultipartUpload_invalid_ETag(s)
CompleteMultipartUpload_success(s)
}
func TestPutBucketAcl(s *S3Conf) {
PutBucketAcl_non_existing_bucket(s)
PutBucketAcl_invalid_acl_canned_and_acp(s)
PutBucketAcl_invalid_acl_canned_and_grants(s)
PutBucketAcl_invalid_acl_acp_and_grants(s)
PutBucketAcl_invalid_owner(s)
PutBucketAcl_success_access_denied(s)
PutBucketAcl_success_grants(s)
PutBucketAcl_success_canned_acl(s)
PutBucketAcl_success_acp(s)
}
func TestGetBucketAcl(s *S3Conf) {
GetBucketAcl_non_existing_bucket(s)
GetBucketAcl_access_denied(s)
GetBucketAcl_success(s)
}
func TestFullFlow(s *S3Conf) {
TestAuthentication(s)
TestCreateBucket(s)
TestHeadBucket(s)
TestListBuckets(s)
TestDeleteBucket(s)
TestPutObject(s)
TestHeadObject(s)
TestGetObject(s)
TestListObjects(s)
TestDeleteObject(s)
TestDeleteObjects(s)
TestCopyObject(s)
TestPutObjectTagging(s)
TestDeleteObjectTagging(s)
TestCreateMultipartUpload(s)
TestUploadPart(s)
TestUploadPartCopy(s)
TestListParts(s)
TestListMultipartUploads(s)
TestAbortMultipartUpload(s)
TestCompleteMultipartUpload(s)
TestPutBucketAcl(s)
TestGetBucketAcl(s)
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +1,41 @@
package integration
import (
"bytes"
"context"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"encoding/xml"
"errors"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"strings"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/aws/smithy-go"
"github.com/versity/versitygw/s3err"
"github.com/versity/versitygw/s3response"
)
var (
bcktCount = 0
succUsrCrt = "The user has been created successfully"
failUsrCrt = "failed to create a user: update iam data: account already exists"
)
func getBucketName() string {
bcktCount++
return fmt.Sprintf("test-bucket-%v", bcktCount)
}
func setup(s *S3Conf, bucket string) error {
s3client := s3.NewFromConfig(s.Config())
@@ -69,6 +95,162 @@ func teardown(s *S3Conf, bucket string) error {
return err
}
func actionHandler(s *S3Conf, testName string, handler func(s3client *s3.Client, bucket string) error) {
runF(testName)
bucketName := getBucketName()
err := setup(s, bucketName)
if err != nil {
failF("%v: failed to create a bucket: %v", testName, err.Error())
return
}
client := s3.NewFromConfig(s.Config())
handlerErr := handler(client, bucketName)
if handlerErr != nil {
failF("%v: %v", testName, handlerErr.Error())
}
err = teardown(s, bucketName)
if err != nil {
if handlerErr == nil {
failF("%v: failed to delete the bucket: %v", testName, err.Error())
} else {
fmt.Printf(colorRed+"%v: failed to delete the bucket: %v", testName, err.Error())
}
}
if handlerErr == nil {
passF(testName)
}
}
type authConfig struct {
testName string
path string
method string
body []byte
service string
date time.Time
}
func authHandler(s *S3Conf, cfg *authConfig, handler func(req *http.Request) error) {
runF(cfg.testName)
req, err := http.NewRequest(cfg.method, fmt.Sprintf("%v/%v", s.endpoint, cfg.path), bytes.NewReader(cfg.body))
if err != nil {
failF("%v: failed to send the request: %v", cfg.testName, err.Error())
return
}
signer := v4.NewSigner()
hashedPayload := sha256.Sum256([]byte{})
hexPayload := hex.EncodeToString(hashedPayload[:])
req.Header.Set("X-Amz-Content-Sha256", hexPayload)
signErr := signer.SignHTTP(req.Context(), aws.Credentials{AccessKeyID: s.awsID, SecretAccessKey: s.awsSecret}, req, hexPayload, cfg.service, s.awsRegion, cfg.date)
if signErr != nil {
failF("%v: failed to sign the request: %v", cfg.testName, err.Error())
return
}
err = handler(req)
if err != nil {
failF("%v: %v", cfg.testName, err.Error())
return
}
passF(cfg.testName)
}
func checkAuthErr(resp *http.Response, apiErr s3err.APIError) error {
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
var errResp s3err.APIErrorResponse
err = xml.Unmarshal(body, &errResp)
if err != nil {
return err
}
if resp.StatusCode != apiErr.HTTPStatusCode {
return fmt.Errorf("expected response status code to be %v, instead got %v", apiErr.HTTPStatusCode, resp.StatusCode)
}
if errResp.Code != apiErr.Code {
return fmt.Errorf("expected error code to be %v, instead got %v", apiErr.Code, errResp.Code)
}
if errResp.Message != apiErr.Description {
return fmt.Errorf("expected error message to be %v, instead got %v", apiErr.Description, errResp.Message)
}
return nil
}
func checkApiErr(err error, apiErr s3err.APIError) error {
if err == nil {
return fmt.Errorf("expected %v, instead got nil", apiErr.Code)
}
var ae smithy.APIError
if errors.As(err, &ae) {
if ae.ErrorCode() == apiErr.Code && ae.ErrorMessage() == apiErr.Description {
return nil
}
return fmt.Errorf("expected %v, instead got %v", apiErr.Code, ae.ErrorCode())
} else {
return fmt.Errorf("expected aws api error, instead got: %v", err.Error())
}
}
func checkSdkApiErr(err error, code string) error {
var ae smithy.APIError
if errors.As(err, &ae) {
if ae.ErrorCode() != code {
return fmt.Errorf("expected %v, instead got %v", ae.ErrorCode(), code)
}
return nil
}
return err
}
func putObjects(client *s3.Client, objs []string, bucket string) error {
for _, key := range objs {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := client.PutObject(ctx, &s3.PutObjectInput{
Key: &key,
Bucket: &bucket,
})
cancel()
if err != nil {
return err
}
}
return nil
}
func putObjectWithData(lgth int64, input *s3.PutObjectInput, client *s3.Client) (csum [32]byte, data []byte, err error) {
data = make([]byte, lgth)
rand.Read(data)
csum = sha256.Sum256(data)
r := bytes.NewReader(data)
input.Body = r
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = client.PutObject(ctx, input)
cancel()
return
}
func CreateMp(s3client *s3.Client, bucket, key string) (*s3.CreateMultipartUploadOutput, error) {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.CreateMultipartUpload(ctx, &s3.CreateMultipartUploadInput{
Bucket: &bucket,
Key: &key,
})
cancel()
return out, err
}
func isEqual(a, b []byte) bool {
if len(a) != len(b) {
return false
@@ -83,32 +265,33 @@ func isEqual(a, b []byte) bool {
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
func compareMultipartUploads(list1, list2 []types.MultipartUpload) bool {
if len(list1) != len(list2) {
return false
}
for i, item := range list1 {
if *item.Key != *list2[i].Key || *item.UploadId != *list2[i].UploadId {
return false
}
}
return false
return true
}
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
}
func compareParts(parts1, parts2 []types.Part) bool {
if len(parts1) != len(parts2) {
return false
}
return false
}
func containsPart(part int32, list []types.Part) bool {
for _, item := range list {
if item.PartNumber == part {
return true
for i, prt := range parts1 {
if prt.PartNumber != parts2[i].PartNumber {
return false
}
if *prt.ETag != *parts2[i].ETag {
return false
}
}
return false
return true
}
func areTagsSame(tags1, tags2 []types.Tag) bool {
@@ -133,7 +316,7 @@ func containsTag(tag types.Tag, list []types.Tag) bool {
return false
}
func checkGrants(grts1, grts2 []types.Grant) bool {
func compareGrants(grts1, grts2 []types.Grant) bool {
if len(grts1) != len(grts2) {
return false
}
@@ -154,3 +337,192 @@ func execCommand(args ...string) ([]byte, error) {
return cmd.CombinedOutput()
}
func getString(str *string) string {
if str == nil {
return ""
}
return *str
}
func getPtr(str string) *string {
return &str
}
func areMapsSame(mp1, mp2 map[string]string) bool {
if len(mp1) != len(mp2) {
return false
}
for key, val := range mp1 {
if mp2[key] != val {
return false
}
}
return true
}
func compareBuckets(list1 []types.Bucket, list2 []s3response.ListAllMyBucketsEntry) bool {
if len(list1) != len(list2) {
return false
}
elementMap := make(map[string]bool)
for _, elem := range list1 {
elementMap[*elem.Name] = true
}
for _, elem := range list2 {
if _, found := elementMap[elem.Name]; !found {
return false
}
}
return true
}
func compareObjects(list1 []string, list2 []types.Object) bool {
if len(list1) != len(list2) {
return false
}
elementMap := make(map[string]bool)
for _, elem := range list1 {
elementMap[elem] = true
}
for _, elem := range list2 {
if _, found := elementMap[*elem.Key]; !found {
return false
}
}
return true
}
func comparePrefixes(list1 []string, list2 []types.CommonPrefix) bool {
if len(list1) != len(list2) {
return false
}
elementMap := make(map[string]bool)
for _, elem := range list1 {
elementMap[elem] = true
}
for _, elem := range list2 {
if _, found := elementMap[*elem.Prefix]; !found {
return false
}
}
return true
}
func compareDelObjects(list1 []string, list2 []types.DeletedObject) bool {
if len(list1) != len(list2) {
return false
}
elementMap := make(map[string]bool)
for _, elem := range list1 {
elementMap[elem] = true
}
for _, elem := range list2 {
if _, found := elementMap[*elem.Key]; !found {
return false
}
}
return true
}
func uploadParts(client *s3.Client, size, partCount int, bucket, key, uploadId string) (parts []types.Part, err error) {
dr := NewDataReader(size, size)
datafile := "rand.data"
w, err := os.Create(datafile)
if err != nil {
return parts, err
}
defer w.Close()
_, err = io.Copy(w, dr)
if err != nil {
return parts, err
}
fileInfo, err := w.Stat()
if err != nil {
return parts, err
}
partSize := fileInfo.Size() / int64(partCount)
var offset int64
for partNumber := int64(1); partNumber <= int64(partCount); partNumber++ {
partStart := (partNumber - 1) * partSize
partEnd := partStart + partSize - 1
if partEnd > fileInfo.Size()-1 {
partEnd = fileInfo.Size() - 1
}
partBuffer := make([]byte, partEnd-partStart+1)
_, err := w.ReadAt(partBuffer, partStart)
if err != nil {
return parts, err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
out, err := client.UploadPart(ctx, &s3.UploadPartInput{
Bucket: &bucket,
Key: &key,
UploadId: &uploadId,
Body: bytes.NewReader(partBuffer),
PartNumber: int32(partNumber),
})
cancel()
if err != nil {
return parts, err
} else {
parts = append(parts, types.Part{ETag: out.ETag, PartNumber: int32(partNumber)})
offset += partSize
}
}
return parts, err
}
type user struct {
access string
secret string
role string
}
func createUsers(s *S3Conf, users []user) error {
for _, usr := range users {
out, err := execCommand("admin", "-a", s.awsID, "-s", s.awsSecret, "-er", s.endpoint, "create-user", "-a", usr.access, "-s", usr.secret, "-r", usr.role)
if err != nil {
return err
}
if !strings.Contains(string(out), succUsrCrt) && !strings.Contains(string(out), failUsrCrt) {
return fmt.Errorf("failed to create a user account")
}
}
return nil
}
func changeBucketsOwner(s *S3Conf, buckets []string, owner string) error {
for _, bucket := range buckets {
out, err := execCommand("admin", "-a", s.awsID, "-s", s.awsSecret, "-er", s.endpoint, "change-bucket-owner", "-b", bucket, "-o", owner)
if err != nil {
return err
}
if !strings.Contains(string(out), "Bucket owner has been updated successfully") {
return fmt.Errorf(string(out))
}
}
return nil
}

43
s3api/admin-router.go Normal file
View File

@@ -0,0 +1,43 @@
// 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 s3api
import (
"github.com/gofiber/fiber/v2"
"github.com/versity/versitygw/auth"
"github.com/versity/versitygw/backend"
"github.com/versity/versitygw/s3api/controllers"
)
type S3AdminRouter struct{}
func (ar *S3AdminRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMService) {
controller := controllers.NewAdminController(iam, be)
// CreateUser admin api
app.Patch("/create-user", controller.CreateUser)
// DeleteUsers admin api
app.Patch("/delete-user", controller.DeleteUser)
// ListUsers admin api
app.Patch("/list-users", controller.ListUsers)
// ChangeBucketOwner admin api
app.Patch("/change-bucket-owner", controller.ChangeBucketOwner)
// ListBucketsAndOwners admin api
app.Patch("/list-buckets", controller.ListBuckets)
}

72
s3api/admin-server.go Normal file
View File

@@ -0,0 +1,72 @@
// 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 s3api
import (
"crypto/tls"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/versity/versitygw/auth"
"github.com/versity/versitygw/backend"
"github.com/versity/versitygw/s3api/middlewares"
)
type S3AdminServer struct {
app *fiber.App
backend backend.Backend
router *S3AdminRouter
port string
cert *tls.Certificate
}
func NewAdminServer(app *fiber.App, be backend.Backend, root middlewares.RootUserConfig, port, region string, iam auth.IAMService, opts ...AdminOpt) *S3AdminServer {
server := &S3AdminServer{
app: app,
backend: be,
router: new(S3AdminRouter),
port: port,
}
for _, opt := range opts {
opt(server)
}
// Logging middlewares
app.Use(logger.New())
app.Use(middlewares.DecodeURL(nil))
// Authentication middlewares
app.Use(middlewares.VerifyV4Signature(root, iam, nil, region, false))
app.Use(middlewares.VerifyMD5Body(nil))
app.Use(middlewares.AclParser(be, nil))
server.router.Init(app, be, iam)
return server
}
type AdminOpt func(s *S3AdminServer)
func WithAdminSrvTLS(cert tls.Certificate) AdminOpt {
return func(s *S3AdminServer) { s.cert = &cert }
}
func (sa *S3AdminServer) Serve() (err error) {
if sa.cert != nil {
return sa.app.ListenTLSWithCertificate(sa.port, *sa.cert)
}
return sa.app.Listen(sa.port)
}

View File

@@ -19,10 +19,16 @@ import (
"github.com/gofiber/fiber/v2"
"github.com/versity/versitygw/auth"
"github.com/versity/versitygw/backend"
)
type AdminController struct {
IAMService auth.IAMService
iam auth.IAMService
be backend.Backend
}
func NewAdminController(iam auth.IAMService, be backend.Backend) AdminController {
return AdminController{iam: iam, be: be}
}
func (c AdminController) CreateUser(ctx *fiber.Ctx) error {
@@ -38,7 +44,7 @@ func (c AdminController) CreateUser(ctx *fiber.Ctx) error {
user := auth.Account{Secret: secret, Role: role}
err := c.IAMService.CreateAccount(access, user)
err := c.iam.CreateAccount(access, user)
if err != nil {
return fmt.Errorf("failed to create a user: %w", err)
}
@@ -53,10 +59,61 @@ func (c AdminController) DeleteUser(ctx *fiber.Ctx) error {
return fmt.Errorf("access denied: only admin users have access to this resource")
}
err := c.IAMService.DeleteUserAccount(access)
err := c.iam.DeleteUserAccount(access)
if err != nil {
return err
}
return ctx.SendString("The user has been deleted successfully")
}
func (c AdminController) ListUsers(ctx *fiber.Ctx) error {
role := ctx.Locals("role").(string)
if role != "admin" {
return fmt.Errorf("access denied: only admin users have access to this resource")
}
accs, err := c.iam.ListUserAccounts()
if err != nil {
return err
}
return ctx.JSON(accs)
}
func (c AdminController) ChangeBucketOwner(ctx *fiber.Ctx) error {
role := ctx.Locals("role").(string)
if role != "admin" {
return fmt.Errorf("access denied: only admin users have access to this resource")
}
owner := ctx.Query("owner")
bucket := ctx.Query("bucket")
accs, err := auth.CheckIfAccountsExist([]string{owner}, c.iam)
if err != nil {
return err
}
if len(accs) > 0 {
return fmt.Errorf("user specified as the new bucket owner does not exist")
}
err = c.be.ChangeBucketOwner(ctx.Context(), bucket, owner)
if err != nil {
return err
}
return ctx.Status(201).SendString("Bucket owner has been updated successfully")
}
func (c AdminController) ListBuckets(ctx *fiber.Ctx) error {
role := ctx.Locals("role").(string)
if role != "admin" {
return fmt.Errorf("access denied: only admin users have access to this resource")
}
buckets, err := c.be.ListBucketsAndOwners(ctx.Context())
if err != nil {
return err
}
return ctx.JSON(buckets)
}

View File

@@ -15,12 +15,15 @@
package controllers
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/gofiber/fiber/v2"
"github.com/versity/versitygw/auth"
"github.com/versity/versitygw/s3response"
)
func TestAdminController_CreateUser(t *testing.T) {
@@ -29,7 +32,7 @@ func TestAdminController_CreateUser(t *testing.T) {
}
adminController := AdminController{
IAMService: &IAMServiceMock{
iam: &IAMServiceMock{
CreateAccountFunc: func(access string, account auth.Account) error {
return nil
},
@@ -108,7 +111,7 @@ func TestAdminController_DeleteUser(t *testing.T) {
}
adminController := AdminController{
IAMService: &IAMServiceMock{
iam: &IAMServiceMock{
DeleteUserAccountFunc: func(access string) error {
return nil
},
@@ -171,3 +174,294 @@ func TestAdminController_DeleteUser(t *testing.T) {
}
}
}
func TestAdminController_ListUsers(t *testing.T) {
type args struct {
req *http.Request
}
adminController := AdminController{
iam: &IAMServiceMock{
ListUserAccountsFunc: func() ([]auth.Account, error) {
return []auth.Account{}, nil
},
},
}
adminControllerErr := AdminController{
iam: &IAMServiceMock{
ListUserAccountsFunc: func() ([]auth.Account, error) {
return []auth.Account{}, fmt.Errorf("server error")
},
},
}
appErr := fiber.New()
appErr.Use(func(ctx *fiber.Ctx) error {
ctx.Locals("role", "admin")
return ctx.Next()
})
appErr.Patch("/list-users", adminControllerErr.ListUsers)
appRoleErr := fiber.New()
appRoleErr.Use(func(ctx *fiber.Ctx) error {
ctx.Locals("role", "user")
return ctx.Next()
})
appRoleErr.Patch("/list-users", adminController.ListUsers)
appSucc := fiber.New()
appSucc.Use(func(ctx *fiber.Ctx) error {
ctx.Locals("role", "admin")
return ctx.Next()
})
appSucc.Patch("/list-users", adminController.ListUsers)
tests := []struct {
name string
app *fiber.App
args args
wantErr bool
statusCode int
}{
{
name: "Admin-list-users-access-denied",
app: appRoleErr,
args: args{
req: httptest.NewRequest(http.MethodPatch, "/list-users", nil),
},
wantErr: false,
statusCode: 500,
},
{
name: "Admin-list-users-iam-error",
app: appErr,
args: args{
req: httptest.NewRequest(http.MethodPatch, "/list-users", nil),
},
wantErr: false,
statusCode: 500,
},
{
name: "Admin-list-users-success",
app: appSucc,
args: args{
req: httptest.NewRequest(http.MethodPatch, "/list-users", nil),
},
wantErr: false,
statusCode: 200,
},
}
for _, tt := range tests {
resp, err := tt.app.Test(tt.args.req)
if (err != nil) != tt.wantErr {
t.Errorf("AdminController.ListUsers() error = %v, wantErr %v", err, tt.wantErr)
}
if resp.StatusCode != tt.statusCode {
t.Errorf("AdminController.ListUsers() statusCode = %v, wantStatusCode = %v", resp.StatusCode, tt.statusCode)
}
}
}
func TestAdminController_ChangeBucketOwner(t *testing.T) {
type args struct {
req *http.Request
}
adminController := AdminController{
be: &BackendMock{
ChangeBucketOwnerFunc: func(contextMoqParam context.Context, bucket, newOwner string) error {
return nil
},
},
iam: &IAMServiceMock{
GetUserAccountFunc: func(access string) (auth.Account, error) {
return auth.Account{}, nil
},
},
}
adminControllerIamErr := AdminController{
iam: &IAMServiceMock{
GetUserAccountFunc: func(access string) (auth.Account, error) {
return auth.Account{}, fmt.Errorf("unknown server error")
},
},
}
adminControllerIamAccDoesNotExist := AdminController{
iam: &IAMServiceMock{
GetUserAccountFunc: func(access string) (auth.Account, error) {
return auth.Account{}, auth.ErrNoSuchUser
},
},
}
app := fiber.New()
app.Use(func(ctx *fiber.Ctx) error {
ctx.Locals("role", "admin")
return ctx.Next()
})
app.Patch("/change-bucket-owner", adminController.ChangeBucketOwner)
appRoleErr := fiber.New()
appRoleErr.Use(func(ctx *fiber.Ctx) error {
ctx.Locals("role", "user")
return ctx.Next()
})
appRoleErr.Patch("/change-bucket-owner", adminController.ChangeBucketOwner)
appIamErr := fiber.New()
appIamErr.Use(func(ctx *fiber.Ctx) error {
ctx.Locals("role", "admin")
return ctx.Next()
})
appIamErr.Patch("/change-bucket-owner", adminControllerIamErr.ChangeBucketOwner)
appIamNoSuchUser := fiber.New()
appIamNoSuchUser.Use(func(ctx *fiber.Ctx) error {
ctx.Locals("role", "admin")
return ctx.Next()
})
appIamNoSuchUser.Patch("/change-bucket-owner", adminControllerIamAccDoesNotExist.ChangeBucketOwner)
tests := []struct {
name string
app *fiber.App
args args
wantErr bool
statusCode int
}{
{
name: "Change-bucket-owner-access-denied",
app: appRoleErr,
args: args{
req: httptest.NewRequest(http.MethodPatch, "/change-bucket-owner", nil),
},
wantErr: false,
statusCode: 500,
},
{
name: "Change-bucket-owner-check-account-server-error",
app: appIamErr,
args: args{
req: httptest.NewRequest(http.MethodPatch, "/change-bucket-owner", nil),
},
wantErr: false,
statusCode: 500,
},
{
name: "Change-bucket-owner-acc-does-not-exist",
app: appIamNoSuchUser,
args: args{
req: httptest.NewRequest(http.MethodPatch, "/change-bucket-owner", nil),
},
wantErr: false,
statusCode: 500,
},
{
name: "Change-bucket-owner-success",
app: app,
args: args{
req: httptest.NewRequest(http.MethodPatch, "/change-bucket-owner?bucket=bucket&owner=owner", nil),
},
wantErr: false,
statusCode: 201,
},
}
for _, tt := range tests {
resp, err := tt.app.Test(tt.args.req)
if (err != nil) != tt.wantErr {
t.Errorf("AdminController.ChangeBucketOwner() error = %v, wantErr %v", err, tt.wantErr)
}
if resp.StatusCode != tt.statusCode {
t.Errorf("AdminController.ChangeBucketOwner() statusCode = %v, wantStatusCode = %v", resp.StatusCode, tt.statusCode)
}
}
}
func TestAdminController_ListBuckets(t *testing.T) {
type args struct {
req *http.Request
}
adminController := AdminController{
be: &BackendMock{
ListBucketsAndOwnersFunc: func(contextMoqParam context.Context) ([]s3response.Bucket, error) {
return []s3response.Bucket{}, nil
},
},
}
app := fiber.New()
app.Use(func(ctx *fiber.Ctx) error {
ctx.Locals("role", "admin")
return ctx.Next()
})
app.Patch("/list-buckets", adminController.ListBuckets)
appRoleErr := fiber.New()
appRoleErr.Use(func(ctx *fiber.Ctx) error {
ctx.Locals("role", "user")
return ctx.Next()
})
appRoleErr.Patch("/list-buckets", adminController.ListBuckets)
tests := []struct {
name string
app *fiber.App
args args
wantErr bool
statusCode int
}{
{
name: "List-buckets-incorrect-role",
app: appRoleErr,
args: args{
req: httptest.NewRequest(http.MethodPatch, "/list-buckets", nil),
},
wantErr: false,
statusCode: 500,
},
{
name: "List-buckets-success",
app: app,
args: args{
req: httptest.NewRequest(http.MethodPatch, "/list-buckets", nil),
},
wantErr: false,
statusCode: 200,
},
}
for _, tt := range tests {
resp, err := tt.app.Test(tt.args.req)
if (err != nil) != tt.wantErr {
t.Errorf("AdminController.ListBuckets() error = %v, wantErr %v", err, tt.wantErr)
}
if resp.StatusCode != tt.statusCode {
t.Errorf("AdminController.ListBuckets() statusCode = %v, wantStatusCode = %v", resp.StatusCode, tt.statusCode)
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -15,6 +15,7 @@
package controllers
import (
"context"
"encoding/json"
"fmt"
"io"
@@ -28,6 +29,7 @@ import (
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/gofiber/fiber/v2"
"github.com/valyala/fasthttp"
"github.com/versity/versitygw/auth"
"github.com/versity/versitygw/backend"
"github.com/versity/versitygw/s3err"
@@ -74,7 +76,7 @@ func TestNew(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := New(tt.args.be, tt.args.iam); !reflect.DeepEqual(got, tt.want) {
if got := New(tt.args.be, tt.args.iam, nil, nil); !reflect.DeepEqual(got, tt.want) {
t.Errorf("New() = %v, want %v", got, tt.want)
}
})
@@ -89,10 +91,7 @@ func TestS3ApiController_ListBuckets(t *testing.T) {
app := fiber.New()
s3ApiController := S3ApiController{
be: &BackendMock{
GetBucketAclFunc: func(bucket string) ([]byte, error) {
return acldata, nil
},
ListBucketsFunc: func() (s3response.ListAllMyBucketsResult, error) {
ListBucketsFunc: func(context.Context, string, bool) (s3response.ListAllMyBucketsResult, error) {
return s3response.ListAllMyBucketsResult{}, nil
},
},
@@ -100,7 +99,7 @@ func TestS3ApiController_ListBuckets(t *testing.T) {
app.Use(func(ctx *fiber.Ctx) error {
ctx.Locals("access", "valid access")
ctx.Locals("isRoot", true)
ctx.Locals("role", "admin")
ctx.Locals("isDebug", false)
return ctx.Next()
})
@@ -110,10 +109,7 @@ func TestS3ApiController_ListBuckets(t *testing.T) {
appErr := fiber.New()
s3ApiControllerErr := S3ApiController{
be: &BackendMock{
GetBucketAclFunc: func(bucket string) ([]byte, error) {
return acldata, nil
},
ListBucketsFunc: func() (s3response.ListAllMyBucketsResult, error) {
ListBucketsFunc: func(context.Context, string, bool) (s3response.ListAllMyBucketsResult, error) {
return s3response.ListAllMyBucketsResult{}, s3err.GetAPIError(s3err.ErrMethodNotAllowed)
},
},
@@ -121,22 +117,12 @@ func TestS3ApiController_ListBuckets(t *testing.T) {
appErr.Use(func(ctx *fiber.Ctx) error {
ctx.Locals("access", "valid access")
ctx.Locals("isRoot", true)
ctx.Locals("role", "admin")
ctx.Locals("isDebug", false)
return ctx.Next()
})
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
@@ -162,15 +148,6 @@ 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) {
@@ -187,32 +164,33 @@ 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
}
getPtr := func(val string) *string {
return &val
}
now := time.Now()
app := fiber.New()
s3ApiController := S3ApiController{
be: &BackendMock{
GetBucketAclFunc: func(bucket string) ([]byte, error) {
GetBucketAclFunc: func(context.Context, *s3.GetBucketAclInput) ([]byte, error) {
return acldata, nil
},
ListObjectPartsFunc: func(bucket, object, uploadID string, partNumberMarker int, maxParts int) (s3response.ListPartsResponse, error) {
return s3response.ListPartsResponse{}, nil
ListPartsFunc: func(context.Context, *s3.ListPartsInput) (s3response.ListPartsResult, error) {
return s3response.ListPartsResult{}, nil
},
GetObjectAclFunc: func(bucket, object string) (*s3.GetObjectAclOutput, error) {
GetObjectAclFunc: func(context.Context, *s3.GetObjectAclInput) (*s3.GetObjectAclOutput, error) {
return &s3.GetObjectAclOutput{}, nil
},
GetObjectAttributesFunc: func(bucket, object string, attributes []string) (*s3.GetObjectAttributesOutput, error) {
GetObjectAttributesFunc: func(context.Context, *s3.GetObjectAttributesInput) (*s3.GetObjectAttributesOutput, error) {
return &s3.GetObjectAttributesOutput{}, nil
},
GetObjectFunc: func(bucket, object, acceptRange string, writer io.Writer) (*s3.GetObjectOutput, error) {
GetObjectFunc: func(context.Context, *s3.GetObjectInput, io.Writer) (*s3.GetObjectOutput, error) {
return &s3.GetObjectOutput{
Metadata: map[string]string{"hello": "world"},
ContentType: getPtr("application/xml"),
@@ -223,7 +201,7 @@ func TestS3ApiController_GetActions(t *testing.T) {
StorageClass: "storage class",
}, nil
},
GetTagsFunc: func(bucket, object string) (map[string]string, error) {
GetTagsFunc: func(_ context.Context, bucket, object string) (map[string]string, error) {
return map[string]string{"hello": "world"}, nil
},
},
@@ -232,6 +210,7 @@ func TestS3ApiController_GetActions(t *testing.T) {
ctx.Locals("access", "valid access")
ctx.Locals("isRoot", true)
ctx.Locals("isDebug", false)
ctx.Locals("parsedAcl", auth.ACL{})
return ctx.Next()
})
app.Get("/:bucket/:key/*", s3ApiController.GetActions)
@@ -352,16 +331,16 @@ func TestS3ApiController_ListActions(t *testing.T) {
app := fiber.New()
s3ApiController := S3ApiController{
be: &BackendMock{
GetBucketAclFunc: func(bucket string) ([]byte, error) {
GetBucketAclFunc: func(context.Context, *s3.GetBucketAclInput) ([]byte, error) {
return acldata, nil
},
ListMultipartUploadsFunc: func(output *s3.ListMultipartUploadsInput) (s3response.ListMultipartUploadsResponse, error) {
return s3response.ListMultipartUploadsResponse{}, nil
ListMultipartUploadsFunc: func(_ context.Context, output *s3.ListMultipartUploadsInput) (s3response.ListMultipartUploadsResult, error) {
return s3response.ListMultipartUploadsResult{}, nil
},
ListObjectsV2Func: func(bucket, prefix, marker, delim string, maxkeys int) (*s3.ListObjectsV2Output, error) {
ListObjectsV2Func: func(context.Context, *s3.ListObjectsV2Input) (*s3.ListObjectsV2Output, error) {
return &s3.ListObjectsV2Output{}, nil
},
ListObjectsFunc: func(bucket, prefix, marker, delim string, maxkeys int) (*s3.ListObjectsOutput, error) {
ListObjectsFunc: func(context.Context, *s3.ListObjectsInput) (*s3.ListObjectsOutput, error) {
return &s3.ListObjectsOutput{}, nil
},
},
@@ -371,6 +350,7 @@ func TestS3ApiController_ListActions(t *testing.T) {
ctx.Locals("access", "valid access")
ctx.Locals("isRoot", true)
ctx.Locals("isDebug", false)
ctx.Locals("parsedAcl", auth.ACL{})
return ctx.Next()
})
@@ -379,10 +359,10 @@ func TestS3ApiController_ListActions(t *testing.T) {
//Error case
s3ApiControllerError := S3ApiController{
be: &BackendMock{
GetBucketAclFunc: func(bucket string) ([]byte, error) {
GetBucketAclFunc: func(context.Context, *s3.GetBucketAclInput) ([]byte, error) {
return acldata, nil
},
ListObjectsFunc: func(bucket, prefix, marker, delim string, maxkeys int) (*s3.ListObjectsOutput, error) {
ListObjectsFunc: func(context.Context, *s3.ListObjectsInput) (*s3.ListObjectsOutput, error) {
return nil, s3err.GetAPIError(s3err.ErrNotImplemented)
},
},
@@ -392,6 +372,7 @@ func TestS3ApiController_ListActions(t *testing.T) {
ctx.Locals("access", "valid access")
ctx.Locals("isRoot", true)
ctx.Locals("isDebug", false)
ctx.Locals("parsedAcl", auth.ACL{})
return ctx.Next()
})
appError.Get("/:bucket", s3ApiControllerError.ListActions)
@@ -495,15 +476,31 @@ func TestS3ApiController_PutBucketActions(t *testing.T) {
</AccessControlPolicy>
`
invOwnerBody := `
<AccessControlPolicy xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<Owner>
<ID>hello</ID>
</Owner>
</AccessControlPolicy>
`
succBody := `
<AccessControlPolicy xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<Owner>
<ID>valid access</ID>
</Owner>
</AccessControlPolicy>
`
s3ApiController := S3ApiController{
be: &BackendMock{
GetBucketAclFunc: func(bucket string) ([]byte, error) {
GetBucketAclFunc: func(context.Context, *s3.GetBucketAclInput) ([]byte, error) {
return acldata, nil
},
PutBucketAclFunc: func(string, []byte) error {
PutBucketAclFunc: func(context.Context, string, []byte) error {
return nil
},
PutBucketFunc: func(bucket, owner string) error {
CreateBucketFunc: func(context.Context, *s3.CreateBucketInput) error {
return nil
},
},
@@ -513,6 +510,7 @@ func TestS3ApiController_PutBucketActions(t *testing.T) {
ctx.Locals("access", "valid access")
ctx.Locals("isRoot", true)
ctx.Locals("isDebug", false)
ctx.Locals("parsedAcl", auth.ACL{Owner: "valid access"})
return ctx.Next()
})
app.Put("/:bucket", s3ApiController.PutBucketActions)
@@ -527,14 +525,12 @@ func TestS3ApiController_PutBucketActions(t *testing.T) {
errAclReq.Header.Set("X-Amz-Grant-Read", "hello")
// PutBucketAcl incorrect bucket owner case
incorrectBucketOwner := httptest.NewRequest(http.MethodPut, "/my-bucket?acl", nil)
incorrectBucketOwner := httptest.NewRequest(http.MethodPut, "/my-bucket?acl", strings.NewReader(invOwnerBody))
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 := httptest.NewRequest(http.MethodPut, "/my-bucket?acl", strings.NewReader(succBody))
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))
@@ -592,6 +588,15 @@ func TestS3ApiController_PutBucketActions(t *testing.T) {
wantErr: false,
statusCode: 200,
},
{
name: "Put-bucket-invalid-bucket-name",
app: app,
args: args{
req: httptest.NewRequest(http.MethodPut, "/aa", nil),
},
wantErr: false,
statusCode: 400,
},
{
name: "Put-bucket-success",
app: app,
@@ -649,25 +654,27 @@ func TestS3ApiController_PutActions(t *testing.T) {
app := fiber.New()
s3ApiController := S3ApiController{
be: &BackendMock{
GetBucketAclFunc: func(bucket string) ([]byte, error) {
GetBucketAclFunc: func(context.Context, *s3.GetBucketAclInput) ([]byte, error) {
return acldata, nil
},
PutObjectAclFunc: func(*s3.PutObjectAclInput) error {
PutObjectAclFunc: func(context.Context, *s3.PutObjectAclInput) error {
return nil
},
CopyObjectFunc: func(srcBucket, srcObject, DstBucket, dstObject string) (*s3.CopyObjectOutput, error) {
return &s3.CopyObjectOutput{}, nil
CopyObjectFunc: func(context.Context, *s3.CopyObjectInput) (*s3.CopyObjectOutput, error) {
return &s3.CopyObjectOutput{
CopyObjectResult: &types.CopyObjectResult{},
}, nil
},
PutObjectFunc: func(*s3.PutObjectInput) (string, error) {
return "Hey", nil
PutObjectFunc: func(context.Context, *s3.PutObjectInput) (string, error) {
return "ETag", nil
},
PutObjectPartFunc: func(bucket, object, uploadID string, part int, length int64, r io.Reader) (string, error) {
UploadPartFunc: func(context.Context, *s3.UploadPartInput) (string, error) {
return "hello", nil
},
SetTagsFunc: func(bucket, object string, tags map[string]string) error {
SetTagsFunc: func(_ context.Context, bucket, object string, tags map[string]string) error {
return nil
},
UploadPartCopyFunc: func(uploadPartCopyInput *s3.UploadPartCopyInput) (s3response.CopyObjectResult, error) {
UploadPartCopyFunc: func(context.Context, *s3.UploadPartCopyInput) (s3response.CopyObjectResult, error) {
return s3response.CopyObjectResult{}, nil
},
},
@@ -676,6 +683,7 @@ func TestS3ApiController_PutActions(t *testing.T) {
ctx.Locals("access", "valid access")
ctx.Locals("isRoot", true)
ctx.Locals("isDebug", false)
ctx.Locals("parsedAcl", auth.ACL{})
return ctx.Next()
})
app.Put("/:bucket/:key/*", s3ApiController.PutActions)
@@ -861,10 +869,10 @@ func TestS3ApiController_DeleteBucket(t *testing.T) {
app := fiber.New()
s3ApiController := S3ApiController{
be: &BackendMock{
GetBucketAclFunc: func(bucket string) ([]byte, error) {
GetBucketAclFunc: func(context.Context, *s3.GetBucketAclInput) ([]byte, error) {
return acldata, nil
},
DeleteBucketFunc: func(bucket string) error {
DeleteBucketFunc: func(context.Context, *s3.DeleteBucketInput) error {
return nil
},
},
@@ -874,6 +882,7 @@ func TestS3ApiController_DeleteBucket(t *testing.T) {
ctx.Locals("access", "valid access")
ctx.Locals("isRoot", true)
ctx.Locals("isDebug", false)
ctx.Locals("parsedAcl", auth.ACL{})
return ctx.Next()
})
@@ -917,11 +926,11 @@ func TestS3ApiController_DeleteObjects(t *testing.T) {
app := fiber.New()
s3ApiController := S3ApiController{
be: &BackendMock{
GetBucketAclFunc: func(bucket string) ([]byte, error) {
GetBucketAclFunc: func(context.Context, *s3.GetBucketAclInput) ([]byte, error) {
return acldata, nil
},
DeleteObjectsFunc: func(bucket string, objects *s3.DeleteObjectsInput) error {
return nil
DeleteObjectsFunc: func(context.Context, *s3.DeleteObjectsInput) (s3response.DeleteObjectsResult, error) {
return s3response.DeleteObjectsResult{}, nil
},
},
}
@@ -930,6 +939,7 @@ func TestS3ApiController_DeleteObjects(t *testing.T) {
ctx.Locals("access", "valid access")
ctx.Locals("isRoot", true)
ctx.Locals("isDebug", false)
ctx.Locals("parsedAcl", auth.ACL{})
return ctx.Next()
})
app.Post("/:bucket", s3ApiController.DeleteObjects)
@@ -987,16 +997,16 @@ func TestS3ApiController_DeleteActions(t *testing.T) {
app := fiber.New()
s3ApiController := S3ApiController{
be: &BackendMock{
GetBucketAclFunc: func(bucket string) ([]byte, error) {
GetBucketAclFunc: func(context.Context, *s3.GetBucketAclInput) ([]byte, error) {
return acldata, nil
},
DeleteObjectFunc: func(bucket, object string) error {
DeleteObjectFunc: func(context.Context, *s3.DeleteObjectInput) error {
return nil
},
AbortMultipartUploadFunc: func(*s3.AbortMultipartUploadInput) error {
AbortMultipartUploadFunc: func(context.Context, *s3.AbortMultipartUploadInput) error {
return nil
},
RemoveTagsFunc: func(bucket, object string) error {
RemoveTagsFunc: func(_ context.Context, bucket, object string) error {
return nil
},
},
@@ -1006,6 +1016,7 @@ func TestS3ApiController_DeleteActions(t *testing.T) {
ctx.Locals("access", "valid access")
ctx.Locals("isRoot", true)
ctx.Locals("isDebug", false)
ctx.Locals("parsedAcl", auth.ACL{})
return ctx.Next()
})
app.Delete("/:bucket/:key/*", s3ApiController.DeleteActions)
@@ -1014,10 +1025,10 @@ func TestS3ApiController_DeleteActions(t *testing.T) {
appErr := fiber.New()
s3ApiControllerErr := S3ApiController{be: &BackendMock{
GetBucketAclFunc: func(bucket string) ([]byte, error) {
GetBucketAclFunc: func(context.Context, *s3.GetBucketAclInput) ([]byte, error) {
return acldata, nil
},
DeleteObjectFunc: func(bucket, object string) error {
DeleteObjectFunc: func(context.Context, *s3.DeleteObjectInput) error {
return s3err.GetAPIError(7)
},
}}
@@ -1026,6 +1037,7 @@ func TestS3ApiController_DeleteActions(t *testing.T) {
ctx.Locals("access", "valid access")
ctx.Locals("isRoot", true)
ctx.Locals("isDebug", false)
ctx.Locals("parsedAcl", auth.ACL{})
return ctx.Next()
})
appErr.Delete("/:bucket/:key/*", s3ApiControllerErr.DeleteActions)
@@ -1095,10 +1107,10 @@ func TestS3ApiController_HeadBucket(t *testing.T) {
app := fiber.New()
s3ApiController := S3ApiController{
be: &BackendMock{
GetBucketAclFunc: func(bucket string) ([]byte, error) {
GetBucketAclFunc: func(context.Context, *s3.GetBucketAclInput) ([]byte, error) {
return acldata, nil
},
HeadBucketFunc: func(bucket string) (*s3.HeadBucketOutput, error) {
HeadBucketFunc: func(context.Context, *s3.HeadBucketInput) (*s3.HeadBucketOutput, error) {
return &s3.HeadBucketOutput{}, nil
},
},
@@ -1108,6 +1120,7 @@ func TestS3ApiController_HeadBucket(t *testing.T) {
ctx.Locals("access", "valid access")
ctx.Locals("isRoot", true)
ctx.Locals("isDebug", false)
ctx.Locals("parsedAcl", auth.ACL{})
return ctx.Next()
})
@@ -1117,10 +1130,10 @@ func TestS3ApiController_HeadBucket(t *testing.T) {
appErr := fiber.New()
s3ApiControllerErr := S3ApiController{be: &BackendMock{
GetBucketAclFunc: func(bucket string) ([]byte, error) {
GetBucketAclFunc: func(context.Context, *s3.GetBucketAclInput) ([]byte, error) {
return acldata, nil
},
HeadBucketFunc: func(bucket string) (*s3.HeadBucketOutput, error) {
HeadBucketFunc: func(context.Context, *s3.HeadBucketInput) (*s3.HeadBucketOutput, error) {
return nil, s3err.GetAPIError(3)
},
},
@@ -1130,6 +1143,7 @@ func TestS3ApiController_HeadBucket(t *testing.T) {
ctx.Locals("access", "valid access")
ctx.Locals("isRoot", true)
ctx.Locals("isDebug", false)
ctx.Locals("parsedAcl", auth.ACL{})
return ctx.Next()
})
@@ -1189,10 +1203,10 @@ func TestS3ApiController_HeadObject(t *testing.T) {
s3ApiController := S3ApiController{
be: &BackendMock{
GetBucketAclFunc: func(bucket string) ([]byte, error) {
GetBucketAclFunc: func(context.Context, *s3.GetBucketAclInput) ([]byte, error) {
return acldata, nil
},
HeadObjectFunc: func(bucket, object string) (*s3.HeadObjectOutput, error) {
HeadObjectFunc: func(context.Context, *s3.HeadObjectInput) (*s3.HeadObjectOutput, error) {
return &s3.HeadObjectOutput{
ContentEncoding: &contentEncoding,
ContentLength: 64,
@@ -1208,6 +1222,7 @@ func TestS3ApiController_HeadObject(t *testing.T) {
ctx.Locals("access", "valid access")
ctx.Locals("isRoot", true)
ctx.Locals("isDebug", false)
ctx.Locals("parsedAcl", auth.ACL{})
return ctx.Next()
})
app.Head("/:bucket/:key/*", s3ApiController.HeadObject)
@@ -1217,10 +1232,10 @@ func TestS3ApiController_HeadObject(t *testing.T) {
s3ApiControllerErr := S3ApiController{
be: &BackendMock{
GetBucketAclFunc: func(bucket string) ([]byte, error) {
GetBucketAclFunc: func(context.Context, *s3.GetBucketAclInput) ([]byte, error) {
return acldata, nil
},
HeadObjectFunc: func(bucket, object string) (*s3.HeadObjectOutput, error) {
HeadObjectFunc: func(context.Context, *s3.HeadObjectInput) (*s3.HeadObjectOutput, error) {
return nil, s3err.GetAPIError(42)
},
},
@@ -1230,6 +1245,7 @@ func TestS3ApiController_HeadObject(t *testing.T) {
ctx.Locals("access", "valid access")
ctx.Locals("isRoot", true)
ctx.Locals("isDebug", false)
ctx.Locals("parsedAcl", auth.ACL{})
return ctx.Next()
})
appErr.Head("/:bucket/:key/*", s3ApiControllerErr.HeadObject)
@@ -1280,25 +1296,36 @@ func TestS3ApiController_CreateActions(t *testing.T) {
app := fiber.New()
s3ApiController := S3ApiController{
be: &BackendMock{
GetBucketAclFunc: func(bucket string) ([]byte, error) {
GetBucketAclFunc: func(context.Context, *s3.GetBucketAclInput) ([]byte, error) {
return acldata, nil
},
RestoreObjectFunc: func(bucket, object string, restoreRequest *s3.RestoreObjectInput) error {
RestoreObjectFunc: func(context.Context, *s3.RestoreObjectInput) error {
return nil
},
CompleteMultipartUploadFunc: func(bucket, object, uploadID string, parts []types.Part) (*s3.CompleteMultipartUploadOutput, error) {
CompleteMultipartUploadFunc: func(context.Context, *s3.CompleteMultipartUploadInput) (*s3.CompleteMultipartUploadOutput, error) {
return &s3.CompleteMultipartUploadOutput{}, nil
},
CreateMultipartUploadFunc: func(*s3.CreateMultipartUploadInput) (*s3.CreateMultipartUploadOutput, error) {
CreateMultipartUploadFunc: func(context.Context, *s3.CreateMultipartUploadInput) (*s3.CreateMultipartUploadOutput, error) {
return &s3.CreateMultipartUploadOutput{}, nil
},
SelectObjectContentFunc: func(contextMoqParam context.Context, selectObjectContentInput *s3.SelectObjectContentInput) (s3response.SelectObjectContentResult, error) {
return s3response.SelectObjectContentResult{}, nil
},
},
}
bdy := `
<SelectObjectContentRequest>
<Expression>string</Expression>
<ExpressionType>string</ExpressionType>
</SelectObjectContentRequest>
`
app.Use(func(ctx *fiber.Ctx) error {
ctx.Locals("access", "valid access")
ctx.Locals("isRoot", true)
ctx.Locals("isDebug", false)
ctx.Locals("parsedAcl", auth.ACL{})
return ctx.Next()
})
app.Post("/:bucket/:key/*", s3ApiController.CreateActions)
@@ -1328,6 +1355,24 @@ func TestS3ApiController_CreateActions(t *testing.T) {
wantErr: false,
statusCode: 500,
},
{
name: "Select-object-content-invalid-body",
app: app,
args: args{
req: httptest.NewRequest(http.MethodPost, "/my-bucket/my-key?select&select-type=2", nil),
},
wantErr: false,
statusCode: 400,
},
{
name: "Select-object-content-invalid-body",
app: app,
args: args{
req: httptest.NewRequest(http.MethodPost, "/my-bucket/my-key?select&select-type=2", strings.NewReader(bdy)),
},
wantErr: false,
statusCode: 200,
},
{
name: "Complete-multipart-upload-error",
app: app,
@@ -1335,7 +1380,7 @@ func TestS3ApiController_CreateActions(t *testing.T) {
req: httptest.NewRequest(http.MethodPost, "/my-bucket/my-key?uploadId=23423", nil),
},
wantErr: false,
statusCode: 500,
statusCode: 400,
},
{
name: "Complete-multipart-upload-success",
@@ -1375,16 +1420,9 @@ func Test_XMLresponse(t *testing.T) {
resp any
err error
}
app := fiber.New()
var ctx fiber.Ctx
// Mocking the fiber ctx
app.Get("/:bucket/:key", func(c *fiber.Ctx) error {
ctx = *c
return nil
})
app.Test(httptest.NewRequest(http.MethodGet, "/my-bucket/my-key", nil))
ctx := app.AcquireCtx(&fasthttp.RequestCtx{})
tests := []struct {
name string
@@ -1395,9 +1433,9 @@ func Test_XMLresponse(t *testing.T) {
{
name: "Internal-server-error",
args: args{
ctx: &ctx,
ctx: ctx,
resp: nil,
err: s3err.GetAPIError(16),
err: s3err.GetAPIError(s3err.ErrInternalError),
},
wantErr: false,
statusCode: 500,
@@ -1405,9 +1443,9 @@ func Test_XMLresponse(t *testing.T) {
{
name: "Error-not-implemented",
args: args{
ctx: &ctx,
ctx: ctx,
resp: nil,
err: s3err.GetAPIError(50),
err: s3err.GetAPIError(s3err.ErrNotImplemented),
},
wantErr: false,
statusCode: 501,
@@ -1415,7 +1453,7 @@ func Test_XMLresponse(t *testing.T) {
{
name: "Invalid-request-body",
args: args{
ctx: &ctx,
ctx: ctx,
resp: make(chan int),
err: nil,
},
@@ -1425,7 +1463,7 @@ func Test_XMLresponse(t *testing.T) {
{
name: "Successful-response",
args: args{
ctx: &ctx,
ctx: ctx,
resp: "Valid response",
err: nil,
},
@@ -1435,7 +1473,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, &MetaOpts{}); (err != nil) != tt.wantErr {
t.Errorf("response() %v error = %v, wantErr %v", tt.name, err, tt.wantErr)
}
@@ -1456,15 +1494,9 @@ func Test_response(t *testing.T) {
resp any
err error
}
app := fiber.New()
var ctx fiber.Ctx
// Mocking the fiber ctx
app.Get("/:bucket/:key", func(c *fiber.Ctx) error {
ctx = *c
return nil
})
app.Test(httptest.NewRequest(http.MethodGet, "/my-bucket/my-key", nil))
app := fiber.New()
ctx := app.AcquireCtx(&fasthttp.RequestCtx{})
tests := []struct {
name string
@@ -1475,9 +1507,9 @@ func Test_response(t *testing.T) {
{
name: "Internal-server-error",
args: args{
ctx: &ctx,
ctx: ctx,
resp: nil,
err: s3err.GetAPIError(16),
err: s3err.GetAPIError(s3err.ErrInternalError),
},
wantErr: false,
statusCode: 500,
@@ -1485,7 +1517,7 @@ func Test_response(t *testing.T) {
{
name: "Internal-server-error-not-api",
args: args{
ctx: &ctx,
ctx: ctx,
resp: nil,
err: fmt.Errorf("custom error"),
},
@@ -1495,9 +1527,9 @@ func Test_response(t *testing.T) {
{
name: "Error-not-implemented",
args: args{
ctx: &ctx,
ctx: ctx,
resp: nil,
err: s3err.GetAPIError(50),
err: s3err.GetAPIError(s3err.ErrNotImplemented),
},
wantErr: false,
statusCode: 501,
@@ -1505,7 +1537,7 @@ func Test_response(t *testing.T) {
{
name: "Successful-response",
args: args{
ctx: &ctx,
ctx: ctx,
resp: "Valid response",
err: nil,
},
@@ -1515,7 +1547,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, &MetaOpts{}); (err != nil) != tt.wantErr {
t.Errorf("response() %v error = %v, wantErr %v", tt.name, err, tt.wantErr)
}

View File

@@ -27,6 +27,9 @@ var _ auth.IAMService = &IAMServiceMock{}
// GetUserAccountFunc: func(access string) (auth.Account, error) {
// panic("mock out the GetUserAccount method")
// },
// ListUserAccountsFunc: func() ([]auth.Account, error) {
// panic("mock out the ListUserAccounts method")
// },
// }
//
// // use mockedIAMService in code that requires auth.IAMService
@@ -43,6 +46,9 @@ type IAMServiceMock struct {
// GetUserAccountFunc mocks the GetUserAccount method.
GetUserAccountFunc func(access string) (auth.Account, error)
// ListUserAccountsFunc mocks the ListUserAccounts method.
ListUserAccountsFunc func() ([]auth.Account, error)
// calls tracks calls to the methods.
calls struct {
// CreateAccount holds details about calls to the CreateAccount method.
@@ -62,10 +68,14 @@ type IAMServiceMock struct {
// Access is the access argument value.
Access string
}
// ListUserAccounts holds details about calls to the ListUserAccounts method.
ListUserAccounts []struct {
}
}
lockCreateAccount sync.RWMutex
lockDeleteUserAccount sync.RWMutex
lockGetUserAccount sync.RWMutex
lockListUserAccounts sync.RWMutex
}
// CreateAccount calls CreateAccountFunc.
@@ -167,3 +177,30 @@ func (mock *IAMServiceMock) GetUserAccountCalls() []struct {
mock.lockGetUserAccount.RUnlock()
return calls
}
// ListUserAccounts calls ListUserAccountsFunc.
func (mock *IAMServiceMock) ListUserAccounts() ([]auth.Account, error) {
if mock.ListUserAccountsFunc == nil {
panic("IAMServiceMock.ListUserAccountsFunc: method is nil but IAMService.ListUserAccounts was just called")
}
callInfo := struct {
}{}
mock.lockListUserAccounts.Lock()
mock.calls.ListUserAccounts = append(mock.calls.ListUserAccounts, callInfo)
mock.lockListUserAccounts.Unlock()
return mock.ListUserAccountsFunc()
}
// ListUserAccountsCalls gets all the calls that were made to ListUserAccounts.
// Check the length with:
//
// len(mockedIAMService.ListUserAccountsCalls())
func (mock *IAMServiceMock) ListUserAccountsCalls() []struct {
} {
var calls []struct {
}
mock.lockListUserAccounts.RLock()
calls = mock.calls.ListUserAccounts
mock.lockListUserAccounts.RUnlock()
return calls
}

View File

@@ -0,0 +1,61 @@
// 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 middlewares
import (
"net/http"
"strings"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/gofiber/fiber/v2"
"github.com/versity/versitygw/auth"
"github.com/versity/versitygw/backend"
"github.com/versity/versitygw/s3api/controllers"
"github.com/versity/versitygw/s3log"
)
func AclParser(be backend.Backend, logger s3log.AuditLogger) fiber.Handler {
return func(ctx *fiber.Ctx) error {
isRoot, access := ctx.Locals("isRoot").(bool), ctx.Locals("access").(string)
path := ctx.Path()
pathParts := strings.Split(path, "/")
bucket := pathParts[1]
if path == "/" && ctx.Method() == http.MethodGet {
return ctx.Next()
}
if ctx.Method() == http.MethodPatch {
return ctx.Next()
}
if len(pathParts) == 2 && pathParts[1] != "" && ctx.Method() == http.MethodPut && !ctx.Request().URI().QueryArgs().Has("acl") {
if err := auth.IsAdmin(access, isRoot); err != nil {
return controllers.SendXMLResponse(ctx, nil, err, &controllers.MetaOpts{Logger: logger, Action: "CreateBucket"})
}
return ctx.Next()
}
//TODO: provide correct action names for the logger, after implementing DetectAction middleware
data, err := be.GetBucketAcl(ctx.Context(), &s3.GetBucketAclInput{Bucket: &bucket})
if err != nil {
return controllers.SendResponse(ctx, err, &controllers.MetaOpts{Logger: logger})
}
parsedAcl, err := auth.ParseACL(data)
if err != nil {
return controllers.SendResponse(ctx, err, &controllers.MetaOpts{Logger: logger})
}
ctx.Locals("parsedAcl", parsedAcl)
return ctx.Next()
}
}

View File

@@ -17,6 +17,9 @@ package middlewares
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"math"
"net/http"
"os"
"strings"
"time"
@@ -29,10 +32,12 @@ import (
"github.com/versity/versitygw/s3api/controllers"
"github.com/versity/versitygw/s3api/utils"
"github.com/versity/versitygw/s3err"
"github.com/versity/versitygw/s3log"
)
const (
iso8601Format = "20060102T150405Z"
YYYYMMDD = "20060102"
)
type RootUserConfig struct {
@@ -40,13 +45,15 @@ 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.AuditLogger, region string, debug bool) fiber.Handler {
acct := accounts{root: root, iam: iam}
return func(ctx *fiber.Ctx) error {
ctx.Locals("region", region)
ctx.Locals("startTime", time.Now())
authorization := ctx.Get("Authorization")
if authorization == "" {
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrAuthHeaderEmpty))
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrAuthHeaderEmpty), &controllers.MetaOpts{Logger: logger})
}
// Check the signature version
@@ -56,48 +63,81 @@ func VerifyV4Signature(root RootUserConfig, iam auth.IAMService, region string,
}
if len(authParts) != 3 {
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrMissingFields))
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrMissingFields), &controllers.MetaOpts{Logger: logger})
}
startParts := strings.Split(authParts[0], " ")
if startParts[0] != "AWS4-HMAC-SHA256" {
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrSignatureVersionNotSupported))
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrSignatureVersionNotSupported), &controllers.MetaOpts{Logger: logger})
}
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.MetaOpts{Logger: logger})
}
// Credential variables validation
creds := strings.Split(credKv[1], "/")
if len(creds) < 4 {
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrCredMalformed))
if len(creds) != 5 {
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrCredMalformed), &controllers.MetaOpts{Logger: logger})
}
if creds[4] != "aws4_request" {
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrSignatureTerminationStr), &controllers.MetaOpts{Logger: logger})
}
if creds[3] != "s3" {
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrSignatureIncorrService), &controllers.MetaOpts{Logger: logger})
}
if creds[2] != region {
return controllers.SendResponse(ctx, s3err.APIError{
Code: "SignatureDoesNotMatch",
Description: fmt.Sprintf("Credential should be scoped to a valid Region, not %v", creds[2]),
HTTPStatusCode: http.StatusForbidden,
}, &controllers.MetaOpts{Logger: logger})
}
ctx.Locals("access", creds[0])
ctx.Locals("isRoot", creds[0] == root.Access)
_, err := time.Parse(YYYYMMDD, creds[1])
if err != nil {
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrSignatureDateDoesNotMatch), &controllers.MetaOpts{Logger: logger})
}
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.MetaOpts{Logger: logger})
}
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.MetaOpts{Logger: logger})
}
if err != nil {
return controllers.SendResponse(ctx, err)
return controllers.SendResponse(ctx, err, &controllers.MetaOpts{Logger: logger})
}
ctx.Locals("role", account.Role)
// 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.MetaOpts{Logger: logger})
}
// 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.MetaOpts{Logger: logger})
}
if date[:8] != creds[1] {
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrSignatureDateDoesNotMatch), &controllers.MetaOpts{Logger: logger})
}
// Validate the dates difference
err = validateDate(tdate)
if err != nil {
return controllers.SendResponse(ctx, err, &controllers.MetaOpts{Logger: logger})
}
hashPayloadHeader := ctx.Get("X-Amz-Content-Sha256")
@@ -110,14 +150,14 @@ func VerifyV4Signature(root RootUserConfig, iam auth.IAMService, region string,
// Compare the calculated hash with the hash provided
if hashPayloadHeader != hexPayload {
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrContentSHA256Mismatch))
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrContentSHA256Mismatch), &controllers.MetaOpts{Logger: logger})
}
}
// 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.MetaOpts{Logger: logger})
}
signer := v4.NewSigner()
@@ -126,30 +166,27 @@ func VerifyV4Signature(root RootUserConfig, iam auth.IAMService, region string,
AccessKeyID: creds[0],
SecretAccessKey: account.Secret,
}, req, hashPayloadHeader, creds[3], region, tdate, func(options *v4.SignerOptions) {
options.DisableURIPathEscaping = true
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.MetaOpts{Logger: logger})
}
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.MetaOpts{Logger: logger})
}
calculatedSign := strings.Split(parts[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.MetaOpts{Logger: logger})
}
ctx.Locals("role", account.Role)
ctx.Locals("access", creds[0])
ctx.Locals("isRoot", creds[0] == root.Access)
return ctx.Next()
}
}
@@ -182,3 +219,27 @@ func isSpecialPayload(str string) bool {
return specialValues[str]
}
func validateDate(date time.Time) error {
now := time.Now().UTC()
diff := date.Unix() - now.Unix()
// Checks the dates difference to be less than a minute
if math.Abs(float64(diff)) > 60 {
if diff > 0 {
return s3err.APIError{
Code: "SignatureDoesNotMatch",
Description: fmt.Sprintf("Signature not yet current: %s is still later than %s", date.Format(iso8601Format), now.Format(iso8601Format)),
HTTPStatusCode: http.StatusForbidden,
}
} else {
return s3err.APIError{
Code: "SignatureDoesNotMatch",
Description: fmt.Sprintf("Signature expired: %s is now earlier than %s", date.Format(iso8601Format), now.Format(iso8601Format)),
HTTPStatusCode: http.StatusForbidden,
}
}
}
return nil
}

View File

@@ -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.AuditLogger) 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.MetaOpts{Logger: logger})
}
return ctx.Next()
}
}

View File

@@ -0,0 +1,41 @@
// 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 middlewares
import (
"net/url"
"github.com/gofiber/fiber/v2"
"github.com/versity/versitygw/s3api/controllers"
"github.com/versity/versitygw/s3err"
"github.com/versity/versitygw/s3log"
)
func DecodeURL(logger s3log.AuditLogger) fiber.Handler {
return func(ctx *fiber.Ctx) error {
reqURL := ctx.Request().URI().String()
decoded, err := url.Parse(reqURL)
if err != nil {
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidURI), &controllers.MetaOpts{Logger: logger})
}
ctx.Path(decoded.Path)
decodedURL, err := url.QueryUnescape(reqURL)
if err != nil {
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidURI), &controllers.MetaOpts{Logger: logger})
}
ctx.Request().SetRequestURI(decodedURL)
return ctx.Next()
}
}

View File

@@ -19,22 +19,40 @@ import (
"github.com/versity/versitygw/auth"
"github.com/versity/versitygw/backend"
"github.com/versity/versitygw/s3api/controllers"
"github.com/versity/versitygw/s3event"
"github.com/versity/versitygw/s3log"
)
type S3ApiRouter struct{}
type S3ApiRouter struct {
WithAdmSrv bool
}
func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMService) {
s3ApiController := controllers.New(be, iam)
adminController := controllers.AdminController{IAMService: iam}
func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMService, logger s3log.AuditLogger, evs s3event.S3EventSender) {
s3ApiController := controllers.New(be, iam, logger, evs)
app.Patch("/create-user", adminController.CreateUser)
if sa.WithAdmSrv {
adminController := controllers.NewAdminController(iam, be)
// CreateUser admin api
app.Patch("/create-user", adminController.CreateUser)
// DeleteUsers admin api
app.Patch("/delete-user", adminController.DeleteUser)
// ListUsers admin api
app.Patch("/list-users", adminController.ListUsers)
// ChangeBucketOwner admin api
app.Patch("/change-bucket-owner", adminController.ChangeBucketOwner)
// ListBucketsAndOwners admin api
app.Patch("/list-buckets", adminController.ListBuckets)
}
// Admin Delete api
app.Patch("/delete-user", adminController.DeleteUser)
// ListBuckets action
app.Get("/", s3ApiController.ListBuckets)
// PutBucket action
// CreateBucket action
// PutBucketAcl action
app.Put("/:bucket", s3ApiController.PutBucketActions)
@@ -43,6 +61,7 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
// HeadBucket
app.Head("/:bucket", s3ApiController.HeadBucket)
// GetBucketAcl action
// ListMultipartUploads action
// ListObjects action
@@ -51,22 +70,34 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
// HeadObject action
app.Head("/:bucket/:key/*", s3ApiController.HeadObject)
// GetObjectAcl action
// GetObject action
// ListObjectParts action
// GetTags action
// ListParts action
// GetObjectAttributes action
app.Get("/:bucket/:key/*", s3ApiController.GetActions)
// DeleteObject action
// AbortMultipartUpload action
// RemoveTags action
app.Delete("/:bucket/:key/*", s3ApiController.DeleteActions)
// DeleteObjects action
app.Post("/:bucket", s3ApiController.DeleteObjects)
// CompleteMultipartUpload action
// CreateMultipartUpload
// RestoreObject action
// SelectObjectContent action
app.Post("/:bucket/:key/*", s3ApiController.CreateActions)
// CopyObject action
// PutObject action
// UploadPart action
// UploadPartCopy action
// SetTags action
// PutObjectAcl action
app.Put("/:bucket/:key/*", s3ApiController.PutActions)
}

View File

@@ -45,7 +45,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, nil, nil)
})
}
}

View File

@@ -22,6 +22,8 @@ import (
"github.com/versity/versitygw/auth"
"github.com/versity/versitygw/backend"
"github.com/versity/versitygw/s3api/middlewares"
"github.com/versity/versitygw/s3event"
"github.com/versity/versitygw/s3log"
)
type S3ApiServer struct {
@@ -33,7 +35,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.AuditLogger, evs s3event.S3EventSender, opts ...Option) (*S3ApiServer, error) {
server := &S3ApiServer{
app: app,
backend: be,
@@ -47,13 +49,15 @@ func New(app *fiber.App, be backend.Backend, root middlewares.RootUserConfig, po
// Logging middlewares
app.Use(logger.New())
app.Use(middlewares.DecodeURL(l))
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))
app.Use(middlewares.AclParser(be, l))
server.router.Init(app, be, iam)
server.router.Init(app, be, iam, l, evs)
return server, nil
}
@@ -66,6 +70,11 @@ func WithTLS(cert tls.Certificate) Option {
return func(s *S3ApiServer) { s.cert = &cert }
}
// WithAdminServer runs admin endpoints with the gateway in the same network
func WithAdminServer() Option {
return func(s *S3ApiServer) { s.router.WithAdmSrv = true }
}
// WithDebug sets debug output
func WithDebug() Option {
return func(s *S3ApiServer) { s.debug = true }

View File

@@ -64,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, nil)
if (err != nil) != tt.wantErr {
t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr)
return

View File

@@ -19,10 +19,18 @@ import (
"errors"
"fmt"
"net/http"
"regexp"
"strconv"
"strings"
"github.com/gofiber/fiber/v2"
"github.com/valyala/fasthttp"
"github.com/versity/versitygw/s3err"
)
var (
bucketNameRegexp = regexp.MustCompile(`^[a-z0-9][a-z0-9.-]+[a-z0-9]$`)
bucketNameIpRegexp = regexp.MustCompile(`^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$`)
)
func GetUserMetaData(headers *fasthttp.RequestHeader) (metadata map[string]string) {
@@ -72,6 +80,17 @@ func SetMetaHeaders(ctx *fiber.Ctx, meta map[string]string) {
}
}
func ParseUint(str string) (int32, error) {
if str == "" {
return -1, nil
}
num, err := strconv.ParseUint(str, 10, 16)
if err != nil {
return -1, s3err.GetAPIError(s3err.ErrInvalidMaxKeys)
}
return int32(num), nil
}
type CustomHeader struct {
Key string
Value string
@@ -83,6 +102,22 @@ func SetResponseHeaders(ctx *fiber.Ctx, headers []CustomHeader) {
}
}
func IsValidBucketName(bucket string) bool {
if len(bucket) < 3 || len(bucket) > 63 {
return false
}
// Checks to contain only digits, lowercase letters, dot, hyphen.
// Checks to start and end with only digits and lowercase letters.
if !bucketNameRegexp.MatchString(bucket) {
return false
}
// Checks not to be a valid IP address
if bucketNameIpRegexp.MatchString(bucket) {
return false
}
return true
}
func includeHeader(hdr string, signedHdrs []string) bool {
for _, shdr := range signedHdrs {
if strings.EqualFold(hdr, shdr) {

View File

@@ -153,3 +153,120 @@ func Test_includeHeader(t *testing.T) {
})
}
}
func TestIsValidBucketName(t *testing.T) {
type args struct {
bucket string
}
tests := []struct {
name string
args args
want bool
}{
{
name: "IsValidBucketName-short-name",
args: args{
bucket: "a",
},
want: false,
},
{
name: "IsValidBucketName-start-with-hyphen",
args: args{
bucket: "-bucket",
},
want: false,
},
{
name: "IsValidBucketName-start-with-dot",
args: args{
bucket: ".bucket",
},
want: false,
},
{
name: "IsValidBucketName-contain-invalid-character",
args: args{
bucket: "my@bucket",
},
want: false,
},
{
name: "IsValidBucketName-end-with-hyphen",
args: args{
bucket: "bucket-",
},
want: false,
},
{
name: "IsValidBucketName-end-with-dot",
args: args{
bucket: "bucket.",
},
want: false,
},
{
name: "IsValidBucketName-valid-bucket-name",
args: args{
bucket: "my-bucket",
},
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := IsValidBucketName(tt.args.bucket); got != tt.want {
t.Errorf("IsValidBucketName() = %v, want %v", got, tt.want)
}
})
}
}
func TestParseUint(t *testing.T) {
type args struct {
str string
}
tests := []struct {
name string
args args
want int32
wantErr bool
}{
{
name: "Parse-uint-empty-string",
args: args{
str: "",
},
want: -1,
wantErr: false,
},
{
name: "Parse-uint-invalid-number-string",
args: args{
str: "bla",
},
want: -1,
wantErr: true,
},
{
name: "Parse-uint-invalid-negative-number",
args: args{
str: "-5",
},
want: -1,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseUint(tt.args.str)
if (err != nil) != tt.wantErr {
t.Errorf("ParseMaxKeys() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("ParseMaxKeys() = %v, want %v", got, tt.want)
}
})
}
}

View File

@@ -97,6 +97,9 @@ const (
ErrNegativeExpires
ErrMaximumExpires
ErrSignatureDoesNotMatch
ErrSignatureDateDoesNotMatch
ErrSignatureTerminationStr
ErrSignatureIncorrService
ErrContentSHA256Mismatch
ErrInvalidAccessKeyID
ErrRequestNotReadyYet
@@ -106,7 +109,10 @@ const (
ErrNotImplemented
ErrPreconditionFailed
ErrInvalidObjectState
ErrInvalidRange
ErrInvalidURI
// Non-AWS errors
ErrExistingObjectIsDirectory
ErrObjectParentIsFile
)
@@ -187,13 +193,11 @@ var errorCodeResponse = map[ErrorCode]APIError{
Description: "We encountered an internal error, please try again.",
HTTPStatusCode: http.StatusInternalServerError,
},
ErrInvalidPart: {
Code: "InvalidPart",
Description: "One or more of the specified parts could not be found. The part may not have been uploaded, or the specified entity tag may not match the part's entity tag.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidCopyDest: {
Code: "InvalidRequest",
Description: "This copy request is illegal because it is trying to copy an object to itself without changing the object's metadata, storage class, website redirect location or encryption attributes.",
@@ -284,7 +288,6 @@ var errorCodeResponse = map[ErrorCode]APIError{
Description: "Signature header missing Signature field.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrUnsignedHeaders: {
Code: "AccessDenied",
Description: "There were headers present in the request which were not signed",
@@ -320,25 +323,36 @@ var errorCodeResponse = map[ErrorCode]APIError{
Description: "X-Amz-Expires must be less than a week (in seconds); that is, the given X-Amz-Expires must be less than 604800 seconds",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidAccessKeyID: {
Code: "InvalidAccessKeyId",
Description: "The access key ID you provided does not exist in our records.",
HTTPStatusCode: http.StatusForbidden,
},
ErrRequestNotReadyYet: {
Code: "AccessDenied",
Description: "Request is not valid yet",
HTTPStatusCode: http.StatusForbidden,
},
ErrSignatureDoesNotMatch: {
Code: "SignatureDoesNotMatch",
Description: "The request signature we calculated does not match the signature you provided. Check your key and signing method.",
HTTPStatusCode: http.StatusForbidden,
},
ErrSignatureDateDoesNotMatch: {
Code: "SignatureDoesNotMatch",
Description: "Date in Credential scope does not match YYYYMMDD from ISO-8601 version of date from HTTP",
HTTPStatusCode: http.StatusForbidden,
},
ErrSignatureTerminationStr: {
Code: "SignatureDoesNotMatch",
Description: "Credential should be scoped with a valid terminator: 'aws4_request'",
HTTPStatusCode: http.StatusForbidden,
},
ErrSignatureIncorrService: {
Code: "SignatureDoesNotMatch",
Description: "Credential should be scoped to correct service: s3",
HTTPStatusCode: http.StatusForbidden,
},
ErrContentSHA256Mismatch: {
Code: "XAmzContentSHA256Mismatch",
Description: "The provided 'x-amz-content-sha256' header does not match what was computed.",
@@ -374,6 +388,16 @@ var errorCodeResponse = map[ErrorCode]APIError{
Description: "The operation is not valid for the current state of the object",
HTTPStatusCode: http.StatusForbidden,
},
ErrInvalidRange: {
Code: "InvalidRange",
Description: "The requested range is not valid for the request. Try another range.",
HTTPStatusCode: http.StatusRequestedRangeNotSatisfiable,
},
ErrInvalidURI: {
Code: "InvalidURI",
Description: "The specified URI couldn't be parsed.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrExistingObjectIsDirectory: {
Code: "ExistingObjectIsDirectory",
Description: "Existing Object is a directory.",

130
s3event/event.go Normal file
View File

@@ -0,0 +1,130 @@
// 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 s3event
import (
"fmt"
"github.com/gofiber/fiber/v2"
)
type S3EventSender interface {
SendEvent(ctx *fiber.Ctx, meta EventMeta)
}
type EventMeta struct {
BucketOwner string
EventName EventType
ObjectSize int64
ObjectETag *string
VersionId *string
}
type EventFields struct {
Records []EventSchema
}
type EventType string
const (
EventObjectPut EventType = "s3:ObjectCreated:Put"
EventObjectCopy EventType = "s3:ObjectCreated:Copy"
EventCompleteMultipartUpload EventType = "s3:ObjectCreated:CompleteMultipartUpload"
EventObjectDelete EventType = "s3:ObjectRemoved:Delete"
EventObjectRestoreCompleted EventType = "s3:ObjectRestore:Completed"
EventObjectTaggingPut EventType = "s3:ObjectTagging:Put"
EventObjectTaggingDelete EventType = "s3:ObjectTagging:Delete"
EventObjectAclPut EventType = "s3:ObjectAcl:Put"
// Not supported
// EventObjectRestorePost EventType = "s3:ObjectRestore:Post"
// EventObjectRestoreDelete EventType = "s3:ObjectRestore:Delete"
)
type EventSchema struct {
EventVersion string `json:"eventVersion"`
EventSource string `json:"eventSource"`
AwsRegion string `json:"awsRegion"`
EventTime string `json:"eventTime"`
EventName EventType `json:"eventName"`
UserIdentity EventUserIdentity `json:"userIdentity"`
RequestParameters EventRequestParams `json:"requestParameters"`
ResponseElements EventResponseElements `json:"responseElements"`
S3 EventS3Data `json:"s3"`
GlacierEventData EventGlacierData `json:"glacierEventData"`
}
type EventUserIdentity struct {
PrincipalId string `json:"PrincipalId"`
}
type EventRequestParams struct {
SourceIPAddress string `json:"sourceIPAddress"`
}
type EventResponseElements struct {
RequestId string `json:"x-amz-request-id"`
HostId string `json:"x-amz-id-2"`
}
type EventS3Data struct {
S3SchemaVersion string `json:"s3SchemaVersion"`
ConfigurationId string `json:"configurationId"`
Bucket EventS3BucketData `json:"bucket"`
Object EventObjectData `json:"object"`
}
type EventGlacierData struct {
RestoreEventData EventRestoreData `json:"restoreEventData"`
}
type EventRestoreData struct {
LifecycleRestorationExpiryTime string `json:"lifecycleRestorationExpiryTime"`
LifecycleRestoreStorageClass string `json:"lifecycleRestoreStorageClass"`
}
type EventS3BucketData struct {
Name string `json:"name"`
OwnerIdentity EventUserIdentity `json:"ownerIdentity"`
Arn string `json:"arn"`
}
type EventObjectData struct {
Key string `json:"key"`
Size int64 `json:"size"`
ETag *string `json:"eTag"`
VersionId *string `json:"versionId"`
Sequencer string `json:"sequencer"`
}
type EventConfig struct {
KafkaURL string
KafkaTopic string
KafkaTopicKey string
NatsURL string
NatsTopic string
}
func InitEventSender(cfg *EventConfig) (S3EventSender, error) {
if cfg.KafkaURL != "" && cfg.NatsURL != "" {
return nil, fmt.Errorf("there should be specified one of the following: kafka, nats")
}
if cfg.NatsURL != "" {
return InitNatsEventService(cfg.NatsURL, cfg.NatsTopic)
}
if cfg.KafkaURL != "" {
return InitKafkaEventService(cfg.KafkaURL, cfg.KafkaTopic, cfg.KafkaTopicKey)
}
return nil, nil
}

153
s3event/kafka.go Normal file
View File

@@ -0,0 +1,153 @@
// 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 s3event
import (
"context"
"encoding/json"
"fmt"
"os"
"strings"
"sync"
"time"
"github.com/gofiber/fiber/v2"
"github.com/segmentio/kafka-go"
)
var sequencer = 0
type Kafka struct {
key string
writer *kafka.Writer
mu sync.Mutex
}
func InitKafkaEventService(url, topic, key string) (S3EventSender, error) {
if topic == "" {
return nil, fmt.Errorf("kafka message topic should be specified")
}
w := kafka.NewWriter(kafka.WriterConfig{
Brokers: []string{url},
Topic: topic,
Balancer: &kafka.LeastBytes{},
BatchTimeout: 5 * time.Millisecond,
})
msg := map[string]string{
"Service": "S3",
"Event": "s3:TestEvent",
"Time": time.Now().Format(time.RFC3339),
"Bucket": "Test-Bucket",
}
msgJSON, err := json.Marshal(msg)
if err != nil {
return nil, err
}
message := kafka.Message{
Key: []byte(key),
Value: msgJSON,
}
ctx := context.Background()
err = w.WriteMessages(ctx, message)
if err != nil {
return nil, err
}
return &Kafka{
key: key,
writer: w,
}, nil
}
func (ks *Kafka) SendEvent(ctx *fiber.Ctx, meta EventMeta) {
ks.mu.Lock()
defer ks.mu.Unlock()
path := strings.Split(ctx.Path(), "/")
bucket, object := path[1], strings.Join(path[2:], "/")
schema := EventSchema{
EventVersion: "2.2",
EventSource: "aws:s3",
AwsRegion: ctx.Locals("region").(string),
EventTime: time.Now().Format(time.RFC3339),
EventName: meta.EventName,
UserIdentity: EventUserIdentity{
PrincipalId: ctx.Locals("access").(string),
},
RequestParameters: EventRequestParams{
SourceIPAddress: ctx.IP(),
},
ResponseElements: EventResponseElements{
RequestId: ctx.Get("X-Amz-Request-Id"),
HostId: ctx.Get("X-Amx-Id-2"),
},
S3: EventS3Data{
S3SchemaVersion: "1.0",
// This field will come up after implementing per bucket notifications
ConfigurationId: "kafka-global",
Bucket: EventS3BucketData{
Name: bucket,
OwnerIdentity: EventUserIdentity{
PrincipalId: ctx.Locals("access").(string),
},
Arn: fmt.Sprintf("arn:aws:s3:::%v", strings.Join(path, "/")),
},
Object: EventObjectData{
Key: object,
Size: meta.ObjectSize,
ETag: meta.ObjectETag,
VersionId: meta.VersionId,
Sequencer: genSequencer(),
},
},
GlacierEventData: EventGlacierData{
// Not supported
RestoreEventData: EventRestoreData{},
},
}
ks.send([]EventSchema{schema})
}
func (ks *Kafka) send(evnt []EventSchema) {
msg, err := json.Marshal(evnt)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to parse the event data: %v\n", err.Error())
return
}
message := kafka.Message{
Key: []byte(ks.key),
Value: msg,
}
ctx := context.Background()
err = ks.writer.WriteMessages(ctx, message)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to send kafka event: %v\n", err.Error())
}
}
func genSequencer() string {
sequencer = sequencer + 1
return fmt.Sprintf("%X", sequencer)
}

112
s3event/nats.go Normal file
View File

@@ -0,0 +1,112 @@
// 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 s3event
import (
"encoding/json"
"fmt"
"os"
"strings"
"sync"
"time"
"github.com/gofiber/fiber/v2"
"github.com/nats-io/nats.go"
)
type NatsEventSender struct {
topic string
client *nats.Conn
mu sync.Mutex
}
func InitNatsEventService(url, topic string) (S3EventSender, error) {
if topic == "" {
return nil, fmt.Errorf("nats message topic should be specified")
}
client, err := nats.Connect(url)
if err != nil {
return nil, err
}
return &NatsEventSender{
topic: topic,
client: client,
}, nil
}
func (ns *NatsEventSender) SendEvent(ctx *fiber.Ctx, meta EventMeta) {
ns.mu.Lock()
defer ns.mu.Unlock()
path := strings.Split(ctx.Path(), "/")
bucket, object := path[1], strings.Join(path[2:], "/")
schema := EventSchema{
EventVersion: "2.2",
EventSource: "aws:s3",
AwsRegion: ctx.Locals("region").(string),
EventTime: time.Now().Format(time.RFC3339),
EventName: meta.EventName,
UserIdentity: EventUserIdentity{
PrincipalId: ctx.Locals("access").(string),
},
RequestParameters: EventRequestParams{
SourceIPAddress: ctx.IP(),
},
ResponseElements: EventResponseElements{
RequestId: ctx.Get("X-Amz-Request-Id"),
HostId: ctx.Get("X-Amx-Id-2"),
},
S3: EventS3Data{
S3SchemaVersion: "1.0",
// This field will come up after implementing per bucket notifications
ConfigurationId: "nats-global",
Bucket: EventS3BucketData{
Name: bucket,
OwnerIdentity: EventUserIdentity{
PrincipalId: ctx.Locals("access").(string),
},
Arn: fmt.Sprintf("arn:aws:s3:::%v", strings.Join(path, "/")),
},
Object: EventObjectData{
Key: object,
Size: meta.ObjectSize,
ETag: meta.ObjectETag,
VersionId: meta.VersionId,
Sequencer: genSequencer(),
},
},
GlacierEventData: EventGlacierData{
// Not supported
RestoreEventData: EventRestoreData{},
},
}
ns.send([]EventSchema{schema})
}
func (ns *NatsEventSender) send(evnt []EventSchema) {
msg, err := json.Marshal(evnt)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to parse the event data: %v\n", err.Error())
}
err = ns.client.Publish(ns.topic, msg)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to send nats event: %v\n", err.Error())
}
}

112
s3log/audit-logger.go Normal file
View File

@@ -0,0 +1,112 @@
// 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 (
"crypto/tls"
"encoding/hex"
"fmt"
"math/rand"
"strings"
"time"
"github.com/gofiber/fiber/v2"
)
type AuditLogger interface {
Log(ctx *fiber.Ctx, err error, body []byte, meta LogMeta)
HangUp() error
Shutdown() error
}
type LogMeta struct {
BucketOwner string
ObjectSize int64
Action string
}
type LogConfig struct {
LogFile string
WebhookURL string
}
type LogFields struct {
BucketOwner string
Bucket string
Time time.Time
RemoteIP string
Requester string
RequestID string
Operation string
Key string
RequestURI string
HttpStatus int
ErrorCode string
BytesSent int
ObjectSize int64
TotalTime int64
TurnAroundTime int64
Referer string
UserAgent string
VersionID string
HostID string
SignatureVersion string
CipherSuite string
AuthenticationType string
HostHeader string
TLSVersion string
AccessPointARN string
AclRequired string
}
func InitLogger(cfg *LogConfig) (AuditLogger, error) {
if cfg.WebhookURL != "" && cfg.LogFile != "" {
return nil, fmt.Errorf("there should be specified one of the following: file, webhook")
}
if cfg.WebhookURL != "" {
return InitWebhookLogger(cfg.WebhookURL)
}
if cfg.LogFile != "" {
return InitFileLogger(cfg.LogFile)
}
return nil, nil
}
func genID() string {
src := rand.New(rand.NewSource(time.Now().UnixNano()))
b := make([]byte, 8)
if _, err := src.Read(b); err != nil {
panic(err)
}
return strings.ToUpper(hex.EncodeToString(b))
}
func getTLSVersionName(version uint16) string {
switch version {
case tls.VersionTLS10:
return "TLSv1.0"
case tls.VersionTLS11:
return "TLSv1.1"
case tls.VersionTLS12:
return "TLSv1.2"
case tls.VersionTLS13:
return "TLSv1.3"
default:
return ""
}
}

230
s3log/file.go Normal file
View File

@@ -0,0 +1,230 @@
// 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 (
"crypto/tls"
"fmt"
"os"
"strings"
"sync"
"time"
"github.com/gofiber/fiber/v2"
"github.com/versity/versitygw/s3err"
)
const (
logFileMode = 0600
timeFormat = "02/January/2006:15:04:05 -0700"
)
// FileLogger is a local file audit log
type FileLogger struct {
logfile string
f *os.File
gotErr bool
mu sync.Mutex
}
var _ AuditLogger = &FileLogger{}
// InitFileLogger initializes audit logs to local file
func InitFileLogger(logname string) (AuditLogger, error) {
f, err := os.OpenFile(logname, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return nil, fmt.Errorf("open log: %w", err)
}
f.WriteString(fmt.Sprintf("log starts %v\n", time.Now()))
return &FileLogger{logfile: logname, f: f}, nil
}
// Log sends log message to file logger
func (f *FileLogger) Log(ctx *fiber.Ctx, err error, body []byte, meta LogMeta) {
f.mu.Lock()
defer f.mu.Unlock()
if f.gotErr {
return
}
lf := LogFields{}
access := "-"
reqURI := ctx.OriginalURL()
path := strings.Split(ctx.Path(), "/")
bucket, object := path[1], strings.Join(path[2:], "/")
errorCode := ""
httpStatus := 200
startTime := ctx.Locals("startTime").(time.Time)
tlsConnState := ctx.Context().TLSConnectionState()
if tlsConnState != nil {
lf.CipherSuite = tls.CipherSuiteName(tlsConnState.CipherSuite)
lf.TLSVersion = getTLSVersionName(tlsConnState.Version)
}
if err != nil {
serr, ok := err.(s3err.APIError)
if ok {
errorCode = serr.Code
httpStatus = serr.HTTPStatusCode
} else {
errorCode = err.Error()
httpStatus = 500
}
}
switch ctx.Locals("access").(type) {
case string:
access = ctx.Locals("access").(string)
}
lf.BucketOwner = meta.BucketOwner
lf.Bucket = bucket
lf.Time = time.Now()
lf.RemoteIP = ctx.IP()
lf.Requester = access
lf.RequestID = genID()
lf.Operation = meta.Action
lf.Key = object
lf.RequestURI = reqURI
lf.HttpStatus = httpStatus
lf.ErrorCode = errorCode
lf.BytesSent = len(body)
lf.ObjectSize = meta.ObjectSize
lf.TotalTime = time.Since(startTime).Milliseconds()
lf.TurnAroundTime = time.Since(startTime).Milliseconds()
lf.Referer = ctx.Get("Referer")
lf.UserAgent = ctx.Get("User-Agent")
lf.VersionID = ctx.Query("versionId")
lf.HostID = ctx.Get("X-Amz-Id-2")
lf.SignatureVersion = "SigV4"
lf.AuthenticationType = "AuthHeader"
lf.HostHeader = fmt.Sprintf("s3.%v.amazonaws.com", ctx.Locals("region").(string))
lf.AccessPointARN = fmt.Sprintf("arn:aws:s3:::%v", strings.Join(path, "/"))
lf.AclRequired = "Yes"
f.writeLog(lf)
}
func (f *FileLogger) writeLog(lf LogFields) {
if lf.BucketOwner == "" {
lf.BucketOwner = "-"
}
if lf.Bucket == "" {
lf.Bucket = "-"
}
if lf.RemoteIP == "" {
lf.RemoteIP = "-"
}
if lf.Requester == "" {
lf.Requester = "-"
}
if lf.Operation == "" {
lf.Operation = "-"
}
if lf.Key == "" {
lf.Key = "-"
}
if lf.RequestURI == "" {
lf.RequestURI = "-"
}
if lf.ErrorCode == "" {
lf.ErrorCode = "-"
}
if lf.Referer == "" {
lf.Referer = "-"
}
if lf.UserAgent == "" {
lf.UserAgent = "-"
}
if lf.VersionID == "" {
lf.VersionID = "-"
}
if lf.HostID == "" {
lf.HostID = "-"
}
if lf.CipherSuite == "" {
lf.CipherSuite = "-"
}
if lf.HostHeader == "" {
lf.HostHeader = "-"
}
if lf.TLSVersion == "" {
lf.TLSVersion = "-"
}
log := fmt.Sprintf("%v %v %v %v %v %v %v %v %v %v %v %v %v %v %v %v %v %v %v %v %v %v %v %v %v %v\n",
lf.BucketOwner,
lf.Bucket,
fmt.Sprintf("[%v]", lf.Time.Format(timeFormat)),
lf.RemoteIP,
lf.Requester,
lf.RequestID,
lf.Operation,
lf.Key,
lf.RequestURI,
lf.HttpStatus,
lf.ErrorCode,
lf.BytesSent,
lf.ObjectSize,
lf.TotalTime,
lf.TurnAroundTime,
lf.Referer,
lf.UserAgent,
lf.VersionID,
lf.HostID,
lf.SignatureVersion,
lf.CipherSuite,
lf.AuthenticationType,
lf.HostHeader,
lf.TLSVersion,
lf.AccessPointARN,
lf.AclRequired,
)
_, err := f.f.WriteString(log)
if err != nil {
fmt.Fprintf(os.Stderr, "error writing to log file: %v\n", err)
// TODO: do we need to terminate on log error?
// set err for now so that we don't spew errors
f.gotErr = true
}
}
// HangUp closes current logfile handle and opens a new one
// typically needed for log rotations
func (f *FileLogger) HangUp() error {
err := f.f.Close()
if err != nil {
return fmt.Errorf("close log: %w", err)
}
f.f, err = os.OpenFile(f.logfile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return fmt.Errorf("open log: %w", err)
}
f.f.WriteString(fmt.Sprintf("log starts %v\n", time.Now()))
return nil
}
// Shutdown closes logfile handle
func (f *FileLogger) Shutdown() error {
return f.f.Close()
}

156
s3log/webhook.go Normal file
View File

@@ -0,0 +1,156 @@
// 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"
"crypto/tls"
"encoding/json"
"fmt"
"net"
"net/http"
"os"
"strings"
"sync"
"time"
"github.com/gofiber/fiber/v2"
"github.com/versity/versitygw/s3err"
)
// WebhookLogger is a webhook URL audit log
type WebhookLogger struct {
mu sync.Mutex
url string
}
var _ AuditLogger = &WebhookLogger{}
// InitWebhookLogger initializes audit logs to webhook URL
func InitWebhookLogger(url string) (AuditLogger, 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: %w", err)
}
}
return &WebhookLogger{
url: url,
}, nil
}
// Log sends log message to webhook
func (wl *WebhookLogger) Log(ctx *fiber.Ctx, err error, body []byte, meta LogMeta) {
wl.mu.Lock()
defer wl.mu.Unlock()
lf := LogFields{}
access := "-"
reqURI := ctx.OriginalURL()
path := strings.Split(ctx.Path(), "/")
bucket, object := path[1], strings.Join(path[2:], "/")
errorCode := ""
httpStatus := 200
startTime := ctx.Locals("startTime").(time.Time)
tlsConnState := ctx.Context().TLSConnectionState()
if tlsConnState != nil {
lf.CipherSuite = tls.CipherSuiteName(tlsConnState.CipherSuite)
lf.TLSVersion = getTLSVersionName(tlsConnState.Version)
}
if err != nil {
serr, ok := err.(s3err.APIError)
if ok {
errorCode = serr.Code
httpStatus = serr.HTTPStatusCode
} else {
errorCode = err.Error()
httpStatus = 500
}
}
switch ctx.Locals("access").(type) {
case string:
access = ctx.Locals("access").(string)
}
lf.BucketOwner = meta.BucketOwner
lf.Bucket = bucket
lf.Time = time.Now()
lf.RemoteIP = ctx.IP()
lf.Requester = access
lf.RequestID = genID()
lf.Operation = meta.Action
lf.Key = object
lf.RequestURI = reqURI
lf.HttpStatus = httpStatus
lf.ErrorCode = errorCode
lf.BytesSent = len(body)
lf.ObjectSize = meta.ObjectSize
lf.TotalTime = time.Since(startTime).Milliseconds()
lf.TurnAroundTime = time.Since(startTime).Milliseconds()
lf.Referer = ctx.Get("Referer")
lf.UserAgent = ctx.Get("User-Agent")
lf.VersionID = ctx.Query("versionId")
lf.HostID = ctx.Get("X-Amz-Id-2")
lf.SignatureVersion = "SigV4"
lf.AuthenticationType = "AuthHeader"
lf.HostHeader = fmt.Sprintf("s3.%v.amazonaws.com", ctx.Locals("region").(string))
lf.AccessPointARN = fmt.Sprintf("arn:aws:s3:::%v", strings.Join(path, "/"))
lf.AclRequired = "Yes"
wl.sendLog(lf)
}
func (wl *WebhookLogger) sendLog(lf LogFields) {
jsonLog, err := json.Marshal(lf)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to parse the log data: %v\n", err.Error())
}
req, err := http.NewRequest(http.MethodPost, wl.url, bytes.NewReader(jsonLog))
if err != nil {
fmt.Fprintln(os.Stderr, err)
}
req.Header.Set("Content-Type", "application/json; charset=utf-8")
go makeRequest(req)
}
func makeRequest(req *http.Request) {
client := &http.Client{
Timeout: 1 * time.Second,
}
_, err := client.Do(req)
if err != nil {
if err, ok := err.(net.Error); ok && !err.Timeout() {
fmt.Fprintf(os.Stderr, "error sending webhook log: %v\n", err)
}
}
}
// HangUp does nothing for webhooks
func (wl *WebhookLogger) HangUp() error {
return nil
}
// Shutdown does nothing for webhooks
func (wl *WebhookLogger) Shutdown() error {
return nil
}

View File

@@ -16,6 +16,8 @@ package s3response
import (
"encoding/xml"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
)
// Part describes part metadata.
@@ -27,7 +29,7 @@ type Part struct {
}
// ListPartsResponse - s3 api list parts response.
type ListPartsResponse struct {
type ListPartsResult struct {
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ ListPartsResult" json:"-"`
Bucket string
@@ -50,7 +52,7 @@ type ListPartsResponse struct {
}
// ListMultipartUploadsResponse - s3 api list multipart uploads response.
type ListMultipartUploadsResponse struct {
type ListMultipartUploadsResult struct {
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ ListMultipartUploadsResult" json:"-"`
Bucket string
@@ -107,3 +109,33 @@ type TagSet struct {
type Tagging struct {
TagSet TagSet `xml:"TagSet"`
}
type DeleteObjects struct {
Objects []types.ObjectIdentifier `xml:"Object"`
}
type DeleteObjectsResult struct {
Deleted []types.DeletedObject
Error []types.Error
}
type SelectObjectContentPayload struct {
Expression *string
ExpressionType types.ExpressionType
RequestProgress *types.RequestProgress
InputSerialization *types.InputSerialization
OutputSerialization *types.OutputSerialization
ScanRange *types.ScanRange
}
type SelectObjectContentResult struct {
Records *types.RecordsEvent
Stats *types.StatsEvent
Progress *types.ProgressEvent
Cont *string
End *string
}
type Bucket struct {
Name string `json:"name"`
Owner string `json:"owner"`
}