mirror of
https://github.com/versity/versitygw.git
synced 2026-04-28 00:06:54 +00:00
fix: fixed merge conflicts
This commit is contained in:
242
auth/acl.go
Normal file
242
auth/acl.go
Normal file
@@ -0,0 +1,242 @@
|
||||
// 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 auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
||||
"github.com/versity/versitygw/s3err"
|
||||
)
|
||||
|
||||
type ACL struct {
|
||||
ACL types.BucketCannedACL
|
||||
Owner string
|
||||
Grantees []Grantee
|
||||
}
|
||||
|
||||
type Grantee struct {
|
||||
Permission types.Permission
|
||||
Access string
|
||||
}
|
||||
|
||||
type GetBucketAclOutput struct {
|
||||
Owner *types.Owner
|
||||
AccessControlList AccessControlList
|
||||
}
|
||||
|
||||
type AccessControlList struct {
|
||||
Grants []types.Grant
|
||||
}
|
||||
|
||||
func ParseACL(data []byte) (ACL, error) {
|
||||
if len(data) == 0 {
|
||||
return ACL{}, nil
|
||||
}
|
||||
|
||||
var acl ACL
|
||||
if err := json.Unmarshal(data, &acl); err != nil {
|
||||
return acl, fmt.Errorf("parse acl: %w", err)
|
||||
}
|
||||
return acl, nil
|
||||
}
|
||||
|
||||
func ParseACLOutput(data []byte) (GetBucketAclOutput, error) {
|
||||
var acl ACL
|
||||
if err := json.Unmarshal(data, &acl); err != nil {
|
||||
return GetBucketAclOutput{}, fmt.Errorf("parse acl: %w", err)
|
||||
}
|
||||
|
||||
grants := []types.Grant{}
|
||||
|
||||
for _, elem := range acl.Grantees {
|
||||
acs := elem.Access
|
||||
grants = append(grants, types.Grant{Grantee: &types.Grantee{ID: &acs}, Permission: elem.Permission})
|
||||
}
|
||||
|
||||
return GetBucketAclOutput{
|
||||
Owner: &types.Owner{
|
||||
ID: &acl.Owner,
|
||||
},
|
||||
AccessControlList: AccessControlList{
|
||||
Grants: grants,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func UpdateACL(input *s3.PutBucketAclInput, acl ACL, iam IAMService) error {
|
||||
if acl.Owner != *input.AccessControlPolicy.Owner.ID {
|
||||
return s3err.GetAPIError(s3err.ErrAccessDenied)
|
||||
}
|
||||
|
||||
// if the ACL is specified, set the ACL, else replace the grantees
|
||||
if input.ACL != "" {
|
||||
acl.ACL = input.ACL
|
||||
acl.Grantees = []Grantee{}
|
||||
return nil
|
||||
}
|
||||
|
||||
grantees := []Grantee{}
|
||||
|
||||
fullControlList, readList, readACPList, writeList, writeACPList := []string{}, []string{}, []string{}, []string{}, []string{}
|
||||
|
||||
if *input.GrantFullControl != "" {
|
||||
fullControlList = splitUnique(*input.GrantFullControl, ",")
|
||||
fmt.Println(fullControlList)
|
||||
for _, str := range fullControlList {
|
||||
grantees = append(grantees, Grantee{Access: str, Permission: "FULL_CONTROL"})
|
||||
}
|
||||
}
|
||||
if *input.GrantRead != "" {
|
||||
readList = splitUnique(*input.GrantRead, ",")
|
||||
for _, str := range readList {
|
||||
grantees = append(grantees, Grantee{Access: str, Permission: "READ"})
|
||||
}
|
||||
}
|
||||
if *input.GrantReadACP != "" {
|
||||
readACPList = splitUnique(*input.GrantReadACP, ",")
|
||||
for _, str := range readACPList {
|
||||
grantees = append(grantees, Grantee{Access: str, Permission: "READ_ACP"})
|
||||
}
|
||||
}
|
||||
if *input.GrantWrite != "" {
|
||||
writeList = splitUnique(*input.GrantWrite, ",")
|
||||
for _, str := range writeList {
|
||||
grantees = append(grantees, Grantee{Access: str, Permission: "WRITE"})
|
||||
}
|
||||
}
|
||||
if *input.GrantWriteACP != "" {
|
||||
writeACPList = splitUnique(*input.GrantWriteACP, ",")
|
||||
for _, str := range writeACPList {
|
||||
grantees = append(grantees, Grantee{Access: str, Permission: "WRITE_ACP"})
|
||||
}
|
||||
}
|
||||
|
||||
accs := append(append(append(append(fullControlList, readList...), writeACPList...), readACPList...), writeList...)
|
||||
|
||||
// Check if the specified accounts exist
|
||||
accList, err := checkIfAccountsExist(accs, iam)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(accList) > 0 {
|
||||
return fmt.Errorf("accounts does not exist: %s", strings.Join(accList, ", "))
|
||||
}
|
||||
|
||||
acl.Grantees = grantees
|
||||
acl.ACL = ""
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkIfAccountsExist(accs []string, iam IAMService) ([]string, error) {
|
||||
result := []string{}
|
||||
|
||||
for _, acc := range accs {
|
||||
_, err := iam.GetUserAccount(acc)
|
||||
if err != nil && err != ErrNoSuchUser {
|
||||
return nil, fmt.Errorf("check user account: %w", err)
|
||||
}
|
||||
if err == nil {
|
||||
result = append(result, acc)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func splitUnique(s, divider string) []string {
|
||||
elements := strings.Split(s, divider)
|
||||
uniqueElements := make(map[string]bool)
|
||||
result := make([]string, 0, len(elements))
|
||||
|
||||
for _, element := range elements {
|
||||
if _, ok := uniqueElements[element]; !ok {
|
||||
result = append(result, element)
|
||||
uniqueElements[element] = true
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func VerifyACL(acl ACL, bucket, access string, permission types.Permission, isRoot bool) error {
|
||||
if isRoot {
|
||||
return nil
|
||||
}
|
||||
|
||||
if acl.Owner == access {
|
||||
return nil
|
||||
}
|
||||
|
||||
if acl.ACL != "" {
|
||||
if (permission == "READ" || permission == "READ_ACP") && (acl.ACL != "public-read" && acl.ACL != "public-read-write") {
|
||||
return s3err.GetAPIError(s3err.ErrAccessDenied)
|
||||
}
|
||||
if (permission == "WRITE" || permission == "WRITE_ACP") && acl.ACL != "public-read-write" {
|
||||
return s3err.GetAPIError(s3err.ErrAccessDenied)
|
||||
}
|
||||
|
||||
return nil
|
||||
} else {
|
||||
grantee := Grantee{Access: access, Permission: permission}
|
||||
granteeFullCtrl := Grantee{Access: access, Permission: "FULL_CONTROL"}
|
||||
|
||||
isFound := false
|
||||
|
||||
for _, grt := range acl.Grantees {
|
||||
if grt == grantee || grt == granteeFullCtrl {
|
||||
isFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if isFound {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return s3err.GetAPIError(s3err.ErrAccessDenied)
|
||||
}
|
||||
|
||||
func IsAdmin(access string, isRoot bool) error {
|
||||
var data IAMConfig
|
||||
|
||||
if isRoot {
|
||||
return nil
|
||||
}
|
||||
|
||||
file, err := os.ReadFile("users.json")
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read config file: %w", err)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(file, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
acc, ok := data.AccessAccounts[access]
|
||||
if !ok {
|
||||
return fmt.Errorf("user does not exist")
|
||||
}
|
||||
|
||||
if acc.Role == "admin" {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("only admin users have access to this resource")
|
||||
}
|
||||
34
auth/iam.go
Normal file
34
auth/iam.go
Normal file
@@ -0,0 +1,34 @@
|
||||
// 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 auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
// Account is a gateway IAM account
|
||||
type Account struct {
|
||||
Secret string `json:"secret"`
|
||||
Role string `json:"role"`
|
||||
}
|
||||
|
||||
// IAMService is the interface for all IAM service implementations
|
||||
type IAMService interface {
|
||||
CreateAccount(access string, account Account) error
|
||||
GetUserAccount(access string) (Account, error)
|
||||
DeleteUserAccount(access string) error
|
||||
}
|
||||
|
||||
var ErrNoSuchUser = errors.New("user not found")
|
||||
178
auth/iam_internal.go
Normal file
178
auth/iam_internal.go
Normal file
@@ -0,0 +1,178 @@
|
||||
// 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 auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"hash/crc32"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// IAMServiceInternal manages the internal IAM service
|
||||
type IAMServiceInternal struct {
|
||||
storer Storer
|
||||
|
||||
mu sync.RWMutex
|
||||
accts IAMConfig
|
||||
serial uint32
|
||||
}
|
||||
|
||||
// UpdateAcctFunc accepts the current data and returns the new data to be stored
|
||||
type UpdateAcctFunc func([]byte) ([]byte, error)
|
||||
|
||||
// Storer is the interface to manage the peristent IAM data for the internal
|
||||
// IAM service
|
||||
type Storer interface {
|
||||
InitIAM() error
|
||||
GetIAM() ([]byte, error)
|
||||
StoreIAM(UpdateAcctFunc) error
|
||||
}
|
||||
|
||||
// IAMConfig stores all internal IAM accounts
|
||||
type IAMConfig struct {
|
||||
AccessAccounts map[string]Account `json:"accessAccounts"`
|
||||
}
|
||||
|
||||
var _ IAMService = &IAMServiceInternal{}
|
||||
|
||||
// NewInternal creates a new instance for the Internal IAM service
|
||||
func NewInternal(s Storer) (*IAMServiceInternal, error) {
|
||||
i := &IAMServiceInternal{
|
||||
storer: s,
|
||||
}
|
||||
|
||||
err := i.updateCache()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("refresh iam cache: %w", err)
|
||||
}
|
||||
|
||||
return i, nil
|
||||
}
|
||||
|
||||
// CreateAccount creates a new IAM account. Returns an error if the account
|
||||
// already exists.
|
||||
func (s *IAMServiceInternal) CreateAccount(access string, account Account) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
return s.storer.StoreIAM(func(data []byte) ([]byte, error) {
|
||||
var conf IAMConfig
|
||||
|
||||
if len(data) > 0 {
|
||||
if err := json.Unmarshal(data, &conf); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse iam: %w", err)
|
||||
}
|
||||
} else {
|
||||
conf.AccessAccounts = make(map[string]Account)
|
||||
}
|
||||
|
||||
_, ok := conf.AccessAccounts[access]
|
||||
if ok {
|
||||
return nil, fmt.Errorf("account already exists")
|
||||
}
|
||||
conf.AccessAccounts[access] = account
|
||||
|
||||
b, err := json.Marshal(s.accts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to serialize iam: %w", err)
|
||||
}
|
||||
|
||||
return b, nil
|
||||
})
|
||||
}
|
||||
|
||||
// GetUserAccount retrieves account info for the requested user. Returns
|
||||
// ErrNoSuchUser if the account does not exist.
|
||||
func (s *IAMServiceInternal) GetUserAccount(access string) (Account, 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)
|
||||
}
|
||||
}
|
||||
|
||||
acct, ok := s.accts.AccessAccounts[access]
|
||||
if !ok {
|
||||
return Account{}, ErrNoSuchUser
|
||||
}
|
||||
|
||||
return acct, nil
|
||||
}
|
||||
|
||||
// updateCache must be called with no locks held
|
||||
func (s *IAMServiceInternal) updateCache() error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
data, err := s.storer.GetIAM()
|
||||
if err != nil {
|
||||
return fmt.Errorf("get iam data: %w", err)
|
||||
}
|
||||
|
||||
serial := crc32.ChecksumIEEE(data)
|
||||
|
||||
if len(data) > 0 {
|
||||
if err := json.Unmarshal(data, &s.accts); err != nil {
|
||||
return fmt.Errorf("failed to parse the config file: %w", err)
|
||||
}
|
||||
} else {
|
||||
s.accts.AccessAccounts = make(map[string]Account)
|
||||
}
|
||||
|
||||
s.serial = serial
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteUserAccount deletes the specified user account. Does not check if
|
||||
// account exists.
|
||||
func (s *IAMServiceInternal) DeleteUserAccount(access string) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
return s.storer.StoreIAM(func(data []byte) ([]byte, error) {
|
||||
if len(data) == 0 {
|
||||
// empty config, do nothing
|
||||
return data, nil
|
||||
}
|
||||
|
||||
var conf IAMConfig
|
||||
|
||||
if err := json.Unmarshal(data, &conf); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse iam: %w", err)
|
||||
}
|
||||
|
||||
delete(conf.AccessAccounts, access)
|
||||
|
||||
b, err := json.Marshal(s.accts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to serialize iam: %w", err)
|
||||
}
|
||||
|
||||
return b, nil
|
||||
})
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
// Copyright 2023 Versity Software
|
||||
// This file is licensed under the Apache License, Version 2.0
|
||||
// (the "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
||||
"github.com/pkg/xattr"
|
||||
"github.com/versity/versitygw/s3err"
|
||||
)
|
||||
|
||||
type ACL struct {
|
||||
ACL types.BucketCannedACL
|
||||
Owner string
|
||||
Grantees []Grantee
|
||||
}
|
||||
|
||||
type Grantee struct {
|
||||
Permission types.Permission
|
||||
Access string
|
||||
}
|
||||
|
||||
type GetBucketAclOutput struct {
|
||||
Owner *types.Owner
|
||||
AccessControlList AccessControlList
|
||||
}
|
||||
|
||||
type AccessControlList struct {
|
||||
Grants []types.Grant
|
||||
}
|
||||
|
||||
type ACLService interface {
|
||||
VerifyACL(bucket, access string, permission types.Permission, isRoot bool) error
|
||||
IsAdmin(access string, isRoot bool) error
|
||||
}
|
||||
|
||||
type ACLServiceUnsupported struct{}
|
||||
|
||||
var _ ACLService = &ACLServiceUnsupported{}
|
||||
|
||||
func (ACLServiceUnsupported) VerifyACL(bucket, access string, permission types.Permission, isRoot bool) error {
|
||||
var ACL ACL
|
||||
|
||||
if isRoot {
|
||||
return nil
|
||||
}
|
||||
|
||||
acl, err := xattr.Get(bucket, "user.acl")
|
||||
if err != nil {
|
||||
return fmt.Errorf("get acl: %w", err)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(acl, &ACL); err != nil {
|
||||
return fmt.Errorf("parse acl: %w", err)
|
||||
}
|
||||
if ACL.Owner == access {
|
||||
return nil
|
||||
}
|
||||
|
||||
if ACL.ACL != "" {
|
||||
if (permission == "READ" || permission == "READ_ACP") && (ACL.ACL != "public-read" && ACL.ACL != "public-read-write") {
|
||||
return s3err.GetAPIError(s3err.ErrAccessDenied)
|
||||
}
|
||||
if (permission == "WRITE" || permission == "WRITE_ACP") && ACL.ACL != "public-read-write" {
|
||||
return s3err.GetAPIError(s3err.ErrAccessDenied)
|
||||
}
|
||||
|
||||
return nil
|
||||
} else {
|
||||
grantee := Grantee{Access: access, Permission: permission}
|
||||
granteeFullCtrl := Grantee{Access: access, Permission: "FULL_CONTROL"}
|
||||
|
||||
isFound := false
|
||||
|
||||
for _, grt := range ACL.Grantees {
|
||||
if grt == grantee || grt == granteeFullCtrl {
|
||||
isFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if isFound {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return s3err.GetAPIError(s3err.ErrAccessDenied)
|
||||
}
|
||||
|
||||
func (ACLServiceUnsupported) IsAdmin(access string, isRoot bool) error {
|
||||
var data IAMConfig
|
||||
|
||||
if isRoot {
|
||||
return nil
|
||||
}
|
||||
|
||||
file, err := os.ReadFile("users.json")
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read config file: %w", err)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(file, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
acc, ok := data.AccessAccounts[access]
|
||||
if !ok {
|
||||
return fmt.Errorf("user does not exist")
|
||||
}
|
||||
|
||||
if acc.Role == "admin" {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("only admin users have access to this resource")
|
||||
}
|
||||
@@ -1,187 +0,0 @@
|
||||
// Copyright 2023 Versity Software
|
||||
// This file is licensed under the Apache License, Version 2.0
|
||||
// (the "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/versity/versitygw/s3err"
|
||||
)
|
||||
|
||||
type Account struct {
|
||||
Secret string `json:"secret"`
|
||||
Role string `json:"role"`
|
||||
Region string `json:"region"`
|
||||
}
|
||||
|
||||
type IAMConfig struct {
|
||||
AccessAccounts map[string]Account `json:"accessAccounts"`
|
||||
}
|
||||
|
||||
type AccountsCache struct {
|
||||
mu sync.Mutex
|
||||
Accounts map[string]Account
|
||||
}
|
||||
|
||||
func (c *AccountsCache) getAccount(access string) *Account {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
acc, ok := c.Accounts[access]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &acc
|
||||
}
|
||||
|
||||
func (c *AccountsCache) updateAccounts() error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
var data IAMConfig
|
||||
|
||||
file, err := os.ReadFile("users.json")
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading config file: %w", err)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(file, &data); err != nil {
|
||||
return fmt.Errorf("error parsing the data: %w", err)
|
||||
}
|
||||
|
||||
c.Accounts = data.AccessAccounts
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *AccountsCache) deleteAccount(access string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
delete(c.Accounts, access)
|
||||
}
|
||||
|
||||
type IAMService interface {
|
||||
GetIAMConfig() (*IAMConfig, error)
|
||||
CreateAccount(access string, account *Account) error
|
||||
GetUserAccount(access string) *Account
|
||||
DeleteUserAccount(access string) error
|
||||
}
|
||||
|
||||
type IAMServiceUnsupported struct {
|
||||
accCache *AccountsCache
|
||||
}
|
||||
|
||||
var _ IAMService = &IAMServiceUnsupported{}
|
||||
|
||||
func InitIAM() (IAMService, error) {
|
||||
_, err := os.ReadFile("users.json")
|
||||
if err != nil {
|
||||
jsonData, err := json.MarshalIndent(IAMConfig{AccessAccounts: map[string]Account{}}, "", " ")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := os.WriteFile("users.json", jsonData, 0644); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &IAMServiceUnsupported{accCache: &AccountsCache{Accounts: map[string]Account{}}}, nil
|
||||
}
|
||||
|
||||
func (IAMServiceUnsupported) GetIAMConfig() (*IAMConfig, error) {
|
||||
return nil, s3err.GetAPIError(s3err.ErrNotImplemented)
|
||||
}
|
||||
|
||||
func (s IAMServiceUnsupported) CreateAccount(access string, account *Account) error {
|
||||
var data IAMConfig
|
||||
|
||||
file, err := os.ReadFile("users.json")
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read config file: %w", err)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(file, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, ok := data.AccessAccounts[access]
|
||||
if ok {
|
||||
return fmt.Errorf("user with the given access already exists")
|
||||
}
|
||||
|
||||
data.AccessAccounts[access] = *account
|
||||
|
||||
updatedJSON, err := json.MarshalIndent(data, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.WriteFile("users.json", updatedJSON, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s IAMServiceUnsupported) GetUserAccount(access string) *Account {
|
||||
acc := s.accCache.getAccount(access)
|
||||
if acc == nil {
|
||||
err := s.accCache.updateAccounts()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return s.accCache.getAccount(access)
|
||||
}
|
||||
|
||||
return acc
|
||||
}
|
||||
|
||||
func (s IAMServiceUnsupported) DeleteUserAccount(access string) error {
|
||||
var data IAMConfig
|
||||
|
||||
file, err := os.ReadFile("users.json")
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read config file: %w", err)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(file, &data); err != nil {
|
||||
return fmt.Errorf("failed to parse the config file: %w", err)
|
||||
}
|
||||
|
||||
_, ok := data.AccessAccounts[access]
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid access for the user: user does not exist")
|
||||
}
|
||||
|
||||
delete(data.AccessAccounts, access)
|
||||
|
||||
updatedJSON, err := json.MarshalIndent(data, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse the data: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile("users.json", updatedJSON, 0644); err != nil {
|
||||
return fmt.Errorf("failed to saved the changes: %w", err)
|
||||
}
|
||||
|
||||
s.accCache.deleteAccount(access)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
||||
"github.com/versity/versitygw/backend/auth"
|
||||
"github.com/versity/versitygw/s3err"
|
||||
"github.com/versity/versitygw/s3response"
|
||||
)
|
||||
@@ -33,9 +32,9 @@ type Backend interface {
|
||||
|
||||
ListBuckets() (s3response.ListAllMyBucketsResult, error)
|
||||
HeadBucket(bucket string) (*s3.HeadBucketOutput, error)
|
||||
GetBucketAcl(bucket string) (*auth.GetBucketAclOutput, error)
|
||||
GetBucketAcl(bucket string) ([]byte, error)
|
||||
PutBucket(bucket, owner string) error
|
||||
PutBucketAcl(*s3.PutBucketAclInput) error
|
||||
PutBucketAcl(bucket string, data []byte) error
|
||||
DeleteBucket(bucket string) error
|
||||
|
||||
CreateMultipartUpload(*s3.CreateMultipartUploadInput) (*s3.CreateMultipartUploadOutput, error)
|
||||
@@ -79,7 +78,7 @@ func (BackendUnsupported) String() string {
|
||||
func (BackendUnsupported) ListBuckets() (s3response.ListAllMyBucketsResult, error) {
|
||||
return s3response.ListAllMyBucketsResult{}, s3err.GetAPIError(s3err.ErrNotImplemented)
|
||||
}
|
||||
func (BackendUnsupported) PutBucketAcl(*s3.PutBucketAclInput) error {
|
||||
func (BackendUnsupported) PutBucketAcl(bucket string, data []byte) error {
|
||||
return s3err.GetAPIError(s3err.ErrNotImplemented)
|
||||
}
|
||||
func (BackendUnsupported) PutObjectAcl(*s3.PutObjectAclInput) error {
|
||||
@@ -91,7 +90,7 @@ func (BackendUnsupported) RestoreObject(bucket, object string, restoreRequest *s
|
||||
func (BackendUnsupported) UploadPartCopy(*s3.UploadPartCopyInput) (*s3.UploadPartCopyOutput, error) {
|
||||
return nil, s3err.GetAPIError(s3err.ErrNotImplemented)
|
||||
}
|
||||
func (BackendUnsupported) GetBucketAcl(bucket string) (*auth.GetBucketAclOutput, error) {
|
||||
func (BackendUnsupported) GetBucketAcl(bucket string) ([]byte, error) {
|
||||
return nil, s3err.GetAPIError(s3err.ErrNotImplemented)
|
||||
}
|
||||
func (BackendUnsupported) HeadBucket(bucket string) (*s3.HeadBucketOutput, error) {
|
||||
|
||||
@@ -6,7 +6,6 @@ package backend
|
||||
import (
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
||||
"github.com/versity/versitygw/backend/auth"
|
||||
"github.com/versity/versitygw/s3response"
|
||||
"io"
|
||||
"sync"
|
||||
@@ -46,7 +45,7 @@ var _ Backend = &BackendMock{}
|
||||
// DeleteObjectsFunc: func(bucket string, objects *s3.DeleteObjectsInput) error {
|
||||
// panic("mock out the DeleteObjects method")
|
||||
// },
|
||||
// GetBucketAclFunc: func(bucket string) (*auth.GetBucketAclOutput, error) {
|
||||
// GetBucketAclFunc: func(bucket string) ([]byte, error) {
|
||||
// panic("mock out the GetBucketAcl method")
|
||||
// },
|
||||
// GetObjectFunc: func(bucket string, object string, acceptRange string, writer io.Writer) (*s3.GetObjectOutput, error) {
|
||||
@@ -85,7 +84,7 @@ var _ Backend = &BackendMock{}
|
||||
// PutBucketFunc: func(bucket string, owner string) error {
|
||||
// panic("mock out the PutBucket method")
|
||||
// },
|
||||
// PutBucketAclFunc: func(putBucketAclInput *s3.PutBucketAclInput) error {
|
||||
// PutBucketAclFunc: func(bucket string, data []byte) error {
|
||||
// panic("mock out the PutBucketAcl method")
|
||||
// },
|
||||
// PutObjectFunc: func(putObjectInput *s3.PutObjectInput) (string, error) {
|
||||
@@ -147,7 +146,7 @@ type BackendMock struct {
|
||||
DeleteObjectsFunc func(bucket string, objects *s3.DeleteObjectsInput) error
|
||||
|
||||
// GetBucketAclFunc mocks the GetBucketAcl method.
|
||||
GetBucketAclFunc func(bucket string) (*auth.GetBucketAclOutput, error)
|
||||
GetBucketAclFunc func(bucket string) ([]byte, error)
|
||||
|
||||
// GetObjectFunc mocks the GetObject method.
|
||||
GetObjectFunc func(bucket string, object string, acceptRange string, writer io.Writer) (*s3.GetObjectOutput, error)
|
||||
@@ -186,7 +185,7 @@ type BackendMock struct {
|
||||
PutBucketFunc func(bucket string, owner string) error
|
||||
|
||||
// PutBucketAclFunc mocks the PutBucketAcl method.
|
||||
PutBucketAclFunc func(putBucketAclInput *s3.PutBucketAclInput) error
|
||||
PutBucketAclFunc func(bucket string, data []byte) error
|
||||
|
||||
// PutObjectFunc mocks the PutObject method.
|
||||
PutObjectFunc func(putObjectInput *s3.PutObjectInput) (string, error)
|
||||
@@ -390,8 +389,10 @@ type BackendMock struct {
|
||||
}
|
||||
// PutBucketAcl holds details about calls to the PutBucketAcl method.
|
||||
PutBucketAcl []struct {
|
||||
// PutBucketAclInput is the putBucketAclInput argument value.
|
||||
PutBucketAclInput *s3.PutBucketAclInput
|
||||
// Bucket is the bucket argument value.
|
||||
Bucket string
|
||||
// Data is the data argument value.
|
||||
Data []byte
|
||||
}
|
||||
// PutObject holds details about calls to the PutObject method.
|
||||
PutObject []struct {
|
||||
@@ -797,7 +798,7 @@ func (mock *BackendMock) DeleteObjectsCalls() []struct {
|
||||
}
|
||||
|
||||
// GetBucketAcl calls GetBucketAclFunc.
|
||||
func (mock *BackendMock) GetBucketAcl(bucket string) (*auth.GetBucketAclOutput, error) {
|
||||
func (mock *BackendMock) GetBucketAcl(bucket string) ([]byte, error) {
|
||||
if mock.GetBucketAclFunc == nil {
|
||||
panic("BackendMock.GetBucketAclFunc: method is nil but Backend.GetBucketAcl was just called")
|
||||
}
|
||||
@@ -1292,19 +1293,21 @@ func (mock *BackendMock) PutBucketCalls() []struct {
|
||||
}
|
||||
|
||||
// PutBucketAcl calls PutBucketAclFunc.
|
||||
func (mock *BackendMock) PutBucketAcl(putBucketAclInput *s3.PutBucketAclInput) error {
|
||||
func (mock *BackendMock) PutBucketAcl(bucket string, data []byte) error {
|
||||
if mock.PutBucketAclFunc == nil {
|
||||
panic("BackendMock.PutBucketAclFunc: method is nil but Backend.PutBucketAcl was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
PutBucketAclInput *s3.PutBucketAclInput
|
||||
Bucket string
|
||||
Data []byte
|
||||
}{
|
||||
PutBucketAclInput: putBucketAclInput,
|
||||
Bucket: bucket,
|
||||
Data: data,
|
||||
}
|
||||
mock.lockPutBucketAcl.Lock()
|
||||
mock.calls.PutBucketAcl = append(mock.calls.PutBucketAcl, callInfo)
|
||||
mock.lockPutBucketAcl.Unlock()
|
||||
return mock.PutBucketAclFunc(putBucketAclInput)
|
||||
return mock.PutBucketAclFunc(bucket, data)
|
||||
}
|
||||
|
||||
// PutBucketAclCalls gets all the calls that were made to PutBucketAcl.
|
||||
@@ -1312,10 +1315,12 @@ func (mock *BackendMock) PutBucketAcl(putBucketAclInput *s3.PutBucketAclInput) e
|
||||
//
|
||||
// len(mockedBackend.PutBucketAclCalls())
|
||||
func (mock *BackendMock) PutBucketAclCalls() []struct {
|
||||
PutBucketAclInput *s3.PutBucketAclInput
|
||||
Bucket string
|
||||
Data []byte
|
||||
} {
|
||||
var calls []struct {
|
||||
PutBucketAclInput *s3.PutBucketAclInput
|
||||
Bucket string
|
||||
Data []byte
|
||||
}
|
||||
mock.lockPutBucketAcl.RLock()
|
||||
calls = mock.calls.PutBucketAcl
|
||||
|
||||
@@ -19,7 +19,6 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
"github.com/versity/versitygw/backend/auth"
|
||||
"github.com/versity/versitygw/s3err"
|
||||
"github.com/versity/versitygw/s3response"
|
||||
)
|
||||
@@ -122,7 +121,7 @@ func TestBackend_GetBucketAcl(t *testing.T) {
|
||||
tests = append(tests, test{
|
||||
name: "get bucket acl error",
|
||||
c: &BackendMock{
|
||||
GetBucketAclFunc: func(bucket string) (*auth.GetBucketAclOutput, error) {
|
||||
GetBucketAclFunc: func(bucket string) ([]byte, error) {
|
||||
return nil, s3err.GetAPIError(s3err.ErrNotImplemented)
|
||||
},
|
||||
},
|
||||
|
||||
@@ -28,32 +28,50 @@ import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/xattr"
|
||||
"github.com/versity/versitygw/auth"
|
||||
"github.com/versity/versitygw/backend"
|
||||
"github.com/versity/versitygw/backend/auth"
|
||||
"github.com/versity/versitygw/s3err"
|
||||
"github.com/versity/versitygw/s3response"
|
||||
)
|
||||
|
||||
type Posix struct {
|
||||
backend.BackendUnsupported
|
||||
|
||||
rootfd *os.File
|
||||
rootdir string
|
||||
backend.BackendUnsupported
|
||||
|
||||
mu sync.RWMutex
|
||||
iamcache []byte
|
||||
iamvalid bool
|
||||
iamexpire time.Time
|
||||
}
|
||||
|
||||
var _ backend.Backend = &Posix{}
|
||||
|
||||
var (
|
||||
cacheDuration = 5 * time.Minute
|
||||
)
|
||||
|
||||
const (
|
||||
metaTmpDir = ".sgwtmp"
|
||||
metaTmpMultipartDir = metaTmpDir + "/multipart"
|
||||
onameAttr = "user.objname"
|
||||
tagHdr = "X-Amz-Tagging"
|
||||
contentTypeHdr = "content-type"
|
||||
contentEncHdr = "content-encoding"
|
||||
emptyMD5 = "d41d8cd98f00b204e9800998ecf8427e"
|
||||
iamFile = "users.json"
|
||||
iamBackupFile = "users.json.backup"
|
||||
aclkey = "user.acl"
|
||||
etagkey = "user.etag"
|
||||
)
|
||||
|
||||
func New(rootdir string) (*Posix, error) {
|
||||
@@ -140,7 +158,7 @@ func (p *Posix) PutBucket(bucket string, owner string) error {
|
||||
return fmt.Errorf("marshal acl: %w", err)
|
||||
}
|
||||
|
||||
if err := xattr.Set(bucket, "user.acl", jsonACL); err != nil {
|
||||
if err := xattr.Set(bucket, aclkey, jsonACL); err != nil {
|
||||
return fmt.Errorf("set acl: %w", err)
|
||||
}
|
||||
|
||||
@@ -263,7 +281,7 @@ func (p *Posix) CompleteMultipartUpload(bucket, object, uploadID string, parts [
|
||||
return nil, s3err.GetAPIError(s3err.ErrInvalidPart)
|
||||
}
|
||||
|
||||
b, err := xattr.Get(partPath, "user.etag")
|
||||
b, err := xattr.Get(partPath, etagkey)
|
||||
etag := string(b)
|
||||
if err != nil {
|
||||
etag = ""
|
||||
@@ -319,7 +337,7 @@ func (p *Posix) CompleteMultipartUpload(bucket, object, uploadID string, parts [
|
||||
// Calculate s3 compatible md5sum for complete multipart.
|
||||
s3MD5 := backend.GetMultipartMD5(parts)
|
||||
|
||||
err = xattr.Set(objname, "user.etag", []byte(s3MD5))
|
||||
err = xattr.Set(objname, etagkey, []byte(s3MD5))
|
||||
if err != nil {
|
||||
// cleanup object if returning error
|
||||
os.Remove(objname)
|
||||
@@ -373,22 +391,22 @@ func loadUserMetaData(path string, m map[string]string) (contentType, contentEnc
|
||||
m[strings.TrimPrefix(e, "user.")] = string(b)
|
||||
}
|
||||
|
||||
b, err := xattr.Get(path, "user.content-type")
|
||||
b, err := xattr.Get(path, "user."+contentTypeHdr)
|
||||
contentType = string(b)
|
||||
if err != nil {
|
||||
contentType = ""
|
||||
}
|
||||
if contentType != "" {
|
||||
m["content-type"] = contentType
|
||||
m[contentTypeHdr] = contentType
|
||||
}
|
||||
|
||||
b, err = xattr.Get(path, "user.content-encoding")
|
||||
b, err = xattr.Get(path, "user."+contentEncHdr)
|
||||
contentEncoding = string(b)
|
||||
if err != nil {
|
||||
contentEncoding = ""
|
||||
}
|
||||
if contentEncoding != "" {
|
||||
m["content-encoding"] = contentEncoding
|
||||
m[contentEncHdr] = contentEncoding
|
||||
}
|
||||
|
||||
return
|
||||
@@ -626,7 +644,7 @@ func (p *Posix) ListObjectParts(bucket, object, uploadID string, partNumberMarke
|
||||
}
|
||||
|
||||
partPath := filepath.Join(objdir, uploadID, e.Name())
|
||||
b, err := xattr.Get(partPath, "user.etag")
|
||||
b, err := xattr.Get(partPath, etagkey)
|
||||
etag := string(b)
|
||||
if err != nil {
|
||||
etag = ""
|
||||
@@ -713,7 +731,7 @@ func (p *Posix) PutObjectPart(bucket, object, uploadID string, part int, length
|
||||
|
||||
dataSum := hash.Sum(nil)
|
||||
etag := hex.EncodeToString(dataSum)
|
||||
xattr.Set(partPath, "user.etag", []byte(etag))
|
||||
xattr.Set(partPath, etagkey, []byte(etag))
|
||||
|
||||
return etag, nil
|
||||
}
|
||||
@@ -741,7 +759,7 @@ func (p *Posix) PutObject(po *s3.PutObjectInput) (string, error) {
|
||||
}
|
||||
|
||||
// set etag attribute to signify this dir was specifically put
|
||||
xattr.Set(name, "user.etag", []byte(emptyMD5))
|
||||
xattr.Set(name, etagkey, []byte(emptyMD5))
|
||||
|
||||
return emptyMD5, nil
|
||||
}
|
||||
@@ -779,7 +797,7 @@ func (p *Posix) PutObject(po *s3.PutObjectInput) (string, error) {
|
||||
|
||||
dataSum := hash.Sum(nil)
|
||||
etag := hex.EncodeToString(dataSum[:])
|
||||
xattr.Set(name, "user.etag", []byte(etag))
|
||||
xattr.Set(name, etagkey, []byte(etag))
|
||||
|
||||
return etag, nil
|
||||
}
|
||||
@@ -816,11 +834,15 @@ func (p *Posix) removeParents(bucket, object string) error {
|
||||
parent := filepath.Dir(objPath)
|
||||
|
||||
if filepath.Base(parent) == bucket {
|
||||
// stop removing parents if we hit the bucket directory.
|
||||
break
|
||||
}
|
||||
|
||||
_, err := xattr.Get(parent, "user.etag")
|
||||
_, err := xattr.Get(parent, etagkey)
|
||||
if err == nil {
|
||||
// a directory with a valid etag means this was specifically
|
||||
// uploaded with a put object, so stop here and leave this
|
||||
// directory in place.
|
||||
break
|
||||
}
|
||||
|
||||
@@ -893,7 +915,7 @@ func (p *Posix) GetObject(bucket, object, acceptRange string, writer io.Writer)
|
||||
|
||||
contentType, contentEncoding := loadUserMetaData(objPath, userMetaData)
|
||||
|
||||
b, err := xattr.Get(objPath, "user.etag")
|
||||
b, err := xattr.Get(objPath, etagkey)
|
||||
etag := string(b)
|
||||
if err != nil {
|
||||
etag = ""
|
||||
@@ -937,7 +959,7 @@ func (p *Posix) HeadObject(bucket, object string) (*s3.HeadObjectOutput, error)
|
||||
userMetaData := make(map[string]string)
|
||||
contentType, contentEncoding := loadUserMetaData(objPath, userMetaData)
|
||||
|
||||
b, err := xattr.Get(objPath, "user.etag")
|
||||
b, err := xattr.Get(objPath, etagkey)
|
||||
etag := string(b)
|
||||
if err != nil {
|
||||
etag = ""
|
||||
@@ -1010,7 +1032,7 @@ func (p *Posix) ListObjects(bucket, prefix, marker, delim string, maxkeys int) (
|
||||
fileSystem := os.DirFS(bucket)
|
||||
results, err := backend.Walk(fileSystem, prefix, delim, marker, maxkeys,
|
||||
func(path string) (bool, error) {
|
||||
_, err := xattr.Get(filepath.Join(bucket, path), "user.etag")
|
||||
_, err := xattr.Get(filepath.Join(bucket, path), etagkey)
|
||||
if isNoAttr(err) {
|
||||
return false, nil
|
||||
}
|
||||
@@ -1019,7 +1041,7 @@ func (p *Posix) ListObjects(bucket, prefix, marker, delim string, maxkeys int) (
|
||||
}
|
||||
return true, nil
|
||||
}, func(path string) (string, error) {
|
||||
etag, err := xattr.Get(filepath.Join(bucket, path), "user.etag")
|
||||
etag, err := xattr.Get(filepath.Join(bucket, path), etagkey)
|
||||
return string(etag), err
|
||||
}, []string{metaTmpDir})
|
||||
if err != nil {
|
||||
@@ -1051,7 +1073,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,
|
||||
func(path string) (bool, error) {
|
||||
_, err := xattr.Get(filepath.Join(bucket, path), "user.etag")
|
||||
_, err := xattr.Get(filepath.Join(bucket, path), etagkey)
|
||||
if isNoAttr(err) {
|
||||
return false, nil
|
||||
}
|
||||
@@ -1060,7 +1082,7 @@ func (p *Posix) ListObjectsV2(bucket, prefix, marker, delim string, maxkeys int)
|
||||
}
|
||||
return true, nil
|
||||
}, func(path string) (string, error) {
|
||||
etag, err := xattr.Get(filepath.Join(bucket, path), "user.etag")
|
||||
etag, err := xattr.Get(filepath.Join(bucket, path), etagkey)
|
||||
return string(etag), err
|
||||
}, []string{metaTmpDir})
|
||||
if err != nil {
|
||||
@@ -1080,115 +1102,39 @@ func (p *Posix) ListObjectsV2(bucket, prefix, marker, delim string, maxkeys int)
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *Posix) PutBucketAcl(input *s3.PutBucketAclInput) error {
|
||||
var ACL auth.ACL
|
||||
acl, err := xattr.Get(*input.Bucket, "user.acl")
|
||||
func (p *Posix) PutBucketAcl(bucket string, data []byte) error {
|
||||
_, err := os.Stat(bucket)
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return s3err.GetAPIError(s3err.ErrNoSuchBucket)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("get acl: %w", err)
|
||||
return fmt.Errorf("stat bucket: %w", err)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(acl, &ACL); err != nil {
|
||||
return fmt.Errorf("parse acl: %w", err)
|
||||
}
|
||||
|
||||
if ACL.Owner != *input.AccessControlPolicy.Owner.ID {
|
||||
return s3err.GetAPIError(s3err.ErrAccessDenied)
|
||||
}
|
||||
|
||||
// if the ACL is specified, set the ACL, else replace the grantees
|
||||
if input.ACL != "" {
|
||||
ACL.ACL = input.ACL
|
||||
ACL.Grantees = []auth.Grantee{}
|
||||
} else {
|
||||
grantees := []auth.Grantee{}
|
||||
|
||||
fullControlList, readList, readACPList, writeList, writeACPList := []string{}, []string{}, []string{}, []string{}, []string{}
|
||||
|
||||
if *input.GrantFullControl != "" {
|
||||
fullControlList = splitUnique(*input.GrantFullControl, ",")
|
||||
fmt.Println(fullControlList)
|
||||
for _, str := range fullControlList {
|
||||
grantees = append(grantees, auth.Grantee{Access: str, Permission: "FULL_CONTROL"})
|
||||
}
|
||||
}
|
||||
if *input.GrantRead != "" {
|
||||
readList = splitUnique(*input.GrantRead, ",")
|
||||
for _, str := range readList {
|
||||
grantees = append(grantees, auth.Grantee{Access: str, Permission: "READ"})
|
||||
}
|
||||
}
|
||||
if *input.GrantReadACP != "" {
|
||||
readACPList = splitUnique(*input.GrantReadACP, ",")
|
||||
for _, str := range readACPList {
|
||||
grantees = append(grantees, auth.Grantee{Access: str, Permission: "READ_ACP"})
|
||||
}
|
||||
}
|
||||
if *input.GrantWrite != "" {
|
||||
writeList = splitUnique(*input.GrantWrite, ",")
|
||||
for _, str := range writeList {
|
||||
grantees = append(grantees, auth.Grantee{Access: str, Permission: "WRITE"})
|
||||
}
|
||||
}
|
||||
if *input.GrantWriteACP != "" {
|
||||
writeACPList = splitUnique(*input.GrantWriteACP, ",")
|
||||
for _, str := range writeACPList {
|
||||
grantees = append(grantees, auth.Grantee{Access: str, Permission: "WRITE_ACP"})
|
||||
}
|
||||
}
|
||||
|
||||
accs := append(append(append(append(fullControlList, readList...), writeACPList...), readACPList...), writeList...)
|
||||
|
||||
// Check if the specified accounts exist
|
||||
accList, err := checkIfAccountsExist(accs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(accList) > 0 {
|
||||
return fmt.Errorf("accounts does not exist: %s", strings.Join(accList, ", "))
|
||||
}
|
||||
|
||||
ACL.Grantees = grantees
|
||||
ACL.ACL = ""
|
||||
}
|
||||
|
||||
ACLJson, err := json.Marshal(ACL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing error: %w", err)
|
||||
}
|
||||
|
||||
if err := xattr.Set(*input.Bucket, "user.acl", ACLJson); err != nil {
|
||||
if err := xattr.Set(bucket, aclkey, data); err != nil {
|
||||
return fmt.Errorf("set acl: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Posix) GetBucketAcl(bucket string) (*auth.GetBucketAclOutput, error) {
|
||||
var ACL auth.ACL
|
||||
acl, err := xattr.Get(bucket, "user.acl")
|
||||
func (p *Posix) GetBucketAcl(bucket string) ([]byte, error) {
|
||||
_, err := os.Stat(bucket)
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return nil, s3err.GetAPIError(s3err.ErrNoSuchBucket)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("stat bucket: %w", err)
|
||||
}
|
||||
|
||||
b, err := xattr.Get(bucket, aclkey)
|
||||
if isNoAttr(err) {
|
||||
return []byte{}, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get acl: %w", err)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(acl, &ACL); err != nil {
|
||||
return nil, fmt.Errorf("parse acl: %w", err)
|
||||
}
|
||||
|
||||
grants := []types.Grant{}
|
||||
|
||||
for _, elem := range ACL.Grantees {
|
||||
acs := elem.Access
|
||||
grants = append(grants, types.Grant{Grantee: &types.Grantee{ID: &acs}, Permission: elem.Permission})
|
||||
}
|
||||
|
||||
return &auth.GetBucketAclOutput{
|
||||
Owner: &types.Owner{
|
||||
ID: &ACL.Owner,
|
||||
},
|
||||
AccessControlList: auth.AccessControlList{
|
||||
Grants: grants,
|
||||
},
|
||||
}, nil
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (p *Posix) GetTags(bucket, object string) (map[string]string, error) {
|
||||
@@ -1264,6 +1210,198 @@ func (p *Posix) RemoveTags(bucket, object string) error {
|
||||
return p.SetTags(bucket, object, nil)
|
||||
}
|
||||
|
||||
const (
|
||||
iamMode = 0600
|
||||
)
|
||||
|
||||
func (p *Posix) InitIAM() error {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
|
||||
_, err := os.ReadFile(iamFile)
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
b, err := json.Marshal(auth.IAMConfig{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal default iam: %w", err)
|
||||
}
|
||||
err = os.WriteFile(iamFile, b, iamMode)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write default iam: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Posix) GetIAM() ([]byte, error) {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
|
||||
if !p.iamvalid || !p.iamexpire.After(time.Now()) {
|
||||
p.mu.RUnlock()
|
||||
err := p.refreshIAM()
|
||||
p.mu.RLock()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return p.iamcache, nil
|
||||
}
|
||||
|
||||
const (
|
||||
backoff = 100 * time.Millisecond
|
||||
maxretry = 300
|
||||
)
|
||||
|
||||
func (p *Posix) refreshIAM() error {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
// We are going to be racing with other running gateways without any
|
||||
// coordination. So we might find the file does not exist at times.
|
||||
// For this case we need to retry for a while assuming the other gateway
|
||||
// will eventually write the file. If it doesn't after the max retries,
|
||||
// then we will return the error.
|
||||
|
||||
retries := 0
|
||||
|
||||
for {
|
||||
b, err := os.ReadFile(iamFile)
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
// racing with someone else updating
|
||||
// keep retrying after backoff
|
||||
retries++
|
||||
if retries < maxretry {
|
||||
time.Sleep(backoff)
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("read iam file: %w", err)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.iamcache = b
|
||||
p.iamvalid = true
|
||||
p.iamexpire = time.Now().Add(cacheDuration)
|
||||
break
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Posix) StoreIAM(update auth.UpdateAcctFunc) error {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
// We are going to be racing with other running gateways without any
|
||||
// coordination. So the strategy here is to read the current file data.
|
||||
// If the file doesn't exist, then we assume someone else is currently
|
||||
// updating the file. So we just need to keep retrying. We also need
|
||||
// to make sure the data is consistent within a single update. So racing
|
||||
// writes to a file would possibly leave this in some invalid state.
|
||||
// We can get atomic updates with rename. If we read the data, update
|
||||
// the data, write to a temp file, then rename the tempfile back to the
|
||||
// data file. This should always result in a complete data image.
|
||||
|
||||
// There is at least one unsolved failure mode here.
|
||||
// If a gateway removes the data file and then crashes, all other
|
||||
// gateways will retry forever thinking that the original will eventually
|
||||
// write the file.
|
||||
|
||||
retries := 0
|
||||
|
||||
for {
|
||||
b, err := os.ReadFile(iamFile)
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
// racing with someone else updating
|
||||
// keep retrying after backoff
|
||||
retries++
|
||||
if retries < maxretry {
|
||||
time.Sleep(backoff)
|
||||
continue
|
||||
}
|
||||
|
||||
// we have been unsuccessful trying to read the iam file
|
||||
// so this must be the case where something happened and
|
||||
// the file did not get updated successfully, and probably
|
||||
// isn't going to be. The recovery procedure would be to
|
||||
// copy the backup file into place of the original.
|
||||
return fmt.Errorf("no iam file, needs backup recovery")
|
||||
}
|
||||
if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
||||
return fmt.Errorf("read iam file: %w", err)
|
||||
}
|
||||
|
||||
// reset retries on successful read
|
||||
retries = 0
|
||||
|
||||
err = os.Remove(iamFile)
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
// racing with someone else updating
|
||||
// keep retrying after backoff
|
||||
time.Sleep(backoff)
|
||||
continue
|
||||
}
|
||||
if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
||||
return fmt.Errorf("remove old iam file: %w", err)
|
||||
}
|
||||
|
||||
// save copy of data
|
||||
datacopy := make([]byte, len(b))
|
||||
copy(datacopy, b)
|
||||
|
||||
// make a backup copy in case we crash before update
|
||||
// this is after remove, so there is a small window something
|
||||
// can go wrong, but the remove should barrier other gateways
|
||||
// from trying to write backup at the same time. Only one
|
||||
// gateway will successfully remove the file.
|
||||
os.WriteFile(iamBackupFile, b, iamMode)
|
||||
|
||||
b, err = update(b)
|
||||
if err != nil {
|
||||
// update failed, try to write old data back out
|
||||
os.WriteFile(iamFile, datacopy, iamMode)
|
||||
return fmt.Errorf("update iam data: %w", err)
|
||||
}
|
||||
|
||||
err = writeTempFile(b)
|
||||
if err != nil {
|
||||
// update failed, try to write old data back out
|
||||
os.WriteFile(iamFile, datacopy, iamMode)
|
||||
return err
|
||||
}
|
||||
|
||||
p.iamcache = b
|
||||
p.iamvalid = true
|
||||
p.iamexpire = time.Now().Add(cacheDuration)
|
||||
break
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeTempFile(b []byte) error {
|
||||
f, err := os.CreateTemp(".", iamFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create temp file: %w", err)
|
||||
}
|
||||
defer os.Remove(f.Name())
|
||||
|
||||
_, err = f.Write(b)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write temp file: %w", err)
|
||||
}
|
||||
|
||||
err = os.Rename(f.Name(), iamFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("rename temp file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isNoAttr(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
@@ -1277,40 +1415,3 @@ func isNoAttr(err error) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func checkIfAccountsExist(accs []string) ([]string, error) {
|
||||
var data auth.IAMConfig
|
||||
result := []string{}
|
||||
|
||||
file, err := os.ReadFile("users.json")
|
||||
if err != nil {
|
||||
return []string{}, fmt.Errorf("unable to read config file: %w", err)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(file, &data); err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
|
||||
for _, acc := range accs {
|
||||
_, ok := data.AccessAccounts[acc]
|
||||
if !ok {
|
||||
result = append(result, acc)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func splitUnique(s, divider string) []string {
|
||||
elements := strings.Split(s, divider)
|
||||
uniqueElements := make(map[string]bool)
|
||||
result := make([]string, 0, len(elements))
|
||||
|
||||
for _, element := range elements {
|
||||
if _, ok := uniqueElements[element]; !ok {
|
||||
result = append(result, element)
|
||||
uniqueElements[element] = true
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -22,8 +22,8 @@ import (
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/versity/versitygw/auth"
|
||||
"github.com/versity/versitygw/backend"
|
||||
"github.com/versity/versitygw/backend/auth"
|
||||
"github.com/versity/versitygw/s3api"
|
||||
"github.com/versity/versitygw/s3api/middlewares"
|
||||
)
|
||||
@@ -133,7 +133,7 @@ func initFlags() []cli.Flag {
|
||||
}
|
||||
}
|
||||
|
||||
func runGateway(be backend.Backend) error {
|
||||
func runGateway(be backend.Backend, s auth.Storer) error {
|
||||
app := fiber.New(fiber.Config{
|
||||
AppName: "versitygw",
|
||||
ServerHeader: "VERSITYGW",
|
||||
@@ -161,16 +161,20 @@ func runGateway(be backend.Backend) error {
|
||||
opts = append(opts, s3api.WithDebug())
|
||||
}
|
||||
|
||||
iam, err := auth.InitIAM()
|
||||
err := s.InitIAM()
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("init iam: %w", err)
|
||||
}
|
||||
|
||||
iam, err := auth.NewInternal(s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("setup internal iam service: %w", err)
|
||||
}
|
||||
|
||||
srv, err := s3api.New(app, be, middlewares.RootUserConfig{
|
||||
Access: rootUserAccess,
|
||||
Secret: rootUserSecret,
|
||||
Region: region,
|
||||
}, port, iam, opts...)
|
||||
}, port, region, iam, opts...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("init gateway: %v", err)
|
||||
}
|
||||
|
||||
@@ -49,5 +49,5 @@ func runPosix(ctx *cli.Context) error {
|
||||
return fmt.Errorf("init posix: %v", err)
|
||||
}
|
||||
|
||||
return runGateway(be)
|
||||
return runGateway(be, be)
|
||||
}
|
||||
|
||||
@@ -52,5 +52,5 @@ func runScoutfs(ctx *cli.Context) error {
|
||||
return fmt.Errorf("init scoutfs: %v", err)
|
||||
}
|
||||
|
||||
return runGateway(be)
|
||||
return runGateway(be, be)
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/versity/versitygw/backend/auth"
|
||||
"github.com/versity/versitygw/auth"
|
||||
)
|
||||
|
||||
type AdminController struct {
|
||||
@@ -26,16 +26,16 @@ type AdminController struct {
|
||||
}
|
||||
|
||||
func (c AdminController) CreateUser(ctx *fiber.Ctx) error {
|
||||
access, secret, role, region := ctx.Query("access"), ctx.Query("secret"), ctx.Query("role"), ctx.Query("region")
|
||||
access, secret, role := ctx.Query("access"), ctx.Query("secret"), ctx.Query("role")
|
||||
requesterRole := ctx.Locals("role")
|
||||
|
||||
if requesterRole != "admin" {
|
||||
return fmt.Errorf("access denied: only admin users have access to this resource")
|
||||
}
|
||||
|
||||
user := auth.Account{Secret: secret, Role: role, Region: region}
|
||||
user := auth.Account{Secret: secret, Role: role}
|
||||
|
||||
err := c.IAMService.CreateAccount(access, &user)
|
||||
err := c.IAMService.CreateAccount(access, user)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create a user: %w", err)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
||||
"github.com/versity/versitygw/backend"
|
||||
"github.com/versity/versitygw/backend/auth"
|
||||
"github.com/versity/versitygw/s3response"
|
||||
"io"
|
||||
"sync"
|
||||
@@ -47,7 +46,7 @@ var _ backend.Backend = &BackendMock{}
|
||||
// DeleteObjectsFunc: func(bucket string, objects *s3.DeleteObjectsInput) error {
|
||||
// panic("mock out the DeleteObjects method")
|
||||
// },
|
||||
// GetBucketAclFunc: func(bucket string) (*auth.GetBucketAclOutput, error) {
|
||||
// GetBucketAclFunc: func(bucket string) ([]byte, error) {
|
||||
// panic("mock out the GetBucketAcl method")
|
||||
// },
|
||||
// GetObjectFunc: func(bucket string, object string, acceptRange string, writer io.Writer) (*s3.GetObjectOutput, error) {
|
||||
@@ -86,7 +85,7 @@ var _ backend.Backend = &BackendMock{}
|
||||
// PutBucketFunc: func(bucket string, owner string) error {
|
||||
// panic("mock out the PutBucket method")
|
||||
// },
|
||||
// PutBucketAclFunc: func(putBucketAclInput *s3.PutBucketAclInput) error {
|
||||
// PutBucketAclFunc: func(bucket string, data []byte) error {
|
||||
// panic("mock out the PutBucketAcl method")
|
||||
// },
|
||||
// PutObjectFunc: func(putObjectInput *s3.PutObjectInput) (string, error) {
|
||||
@@ -148,7 +147,7 @@ type BackendMock struct {
|
||||
DeleteObjectsFunc func(bucket string, objects *s3.DeleteObjectsInput) error
|
||||
|
||||
// GetBucketAclFunc mocks the GetBucketAcl method.
|
||||
GetBucketAclFunc func(bucket string) (*auth.GetBucketAclOutput, error)
|
||||
GetBucketAclFunc func(bucket string) ([]byte, error)
|
||||
|
||||
// GetObjectFunc mocks the GetObject method.
|
||||
GetObjectFunc func(bucket string, object string, acceptRange string, writer io.Writer) (*s3.GetObjectOutput, error)
|
||||
@@ -187,7 +186,7 @@ type BackendMock struct {
|
||||
PutBucketFunc func(bucket string, owner string) error
|
||||
|
||||
// PutBucketAclFunc mocks the PutBucketAcl method.
|
||||
PutBucketAclFunc func(putBucketAclInput *s3.PutBucketAclInput) error
|
||||
PutBucketAclFunc func(bucket string, data []byte) error
|
||||
|
||||
// PutObjectFunc mocks the PutObject method.
|
||||
PutObjectFunc func(putObjectInput *s3.PutObjectInput) (string, error)
|
||||
@@ -391,8 +390,10 @@ type BackendMock struct {
|
||||
}
|
||||
// PutBucketAcl holds details about calls to the PutBucketAcl method.
|
||||
PutBucketAcl []struct {
|
||||
// PutBucketAclInput is the putBucketAclInput argument value.
|
||||
PutBucketAclInput *s3.PutBucketAclInput
|
||||
// Bucket is the bucket argument value.
|
||||
Bucket string
|
||||
// Data is the data argument value.
|
||||
Data []byte
|
||||
}
|
||||
// PutObject holds details about calls to the PutObject method.
|
||||
PutObject []struct {
|
||||
@@ -798,7 +799,7 @@ func (mock *BackendMock) DeleteObjectsCalls() []struct {
|
||||
}
|
||||
|
||||
// GetBucketAcl calls GetBucketAclFunc.
|
||||
func (mock *BackendMock) GetBucketAcl(bucket string) (*auth.GetBucketAclOutput, error) {
|
||||
func (mock *BackendMock) GetBucketAcl(bucket string) ([]byte, error) {
|
||||
if mock.GetBucketAclFunc == nil {
|
||||
panic("BackendMock.GetBucketAclFunc: method is nil but Backend.GetBucketAcl was just called")
|
||||
}
|
||||
@@ -1293,19 +1294,21 @@ func (mock *BackendMock) PutBucketCalls() []struct {
|
||||
}
|
||||
|
||||
// PutBucketAcl calls PutBucketAclFunc.
|
||||
func (mock *BackendMock) PutBucketAcl(putBucketAclInput *s3.PutBucketAclInput) error {
|
||||
func (mock *BackendMock) PutBucketAcl(bucket string, data []byte) error {
|
||||
if mock.PutBucketAclFunc == nil {
|
||||
panic("BackendMock.PutBucketAclFunc: method is nil but Backend.PutBucketAcl was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
PutBucketAclInput *s3.PutBucketAclInput
|
||||
Bucket string
|
||||
Data []byte
|
||||
}{
|
||||
PutBucketAclInput: putBucketAclInput,
|
||||
Bucket: bucket,
|
||||
Data: data,
|
||||
}
|
||||
mock.lockPutBucketAcl.Lock()
|
||||
mock.calls.PutBucketAcl = append(mock.calls.PutBucketAcl, callInfo)
|
||||
mock.lockPutBucketAcl.Unlock()
|
||||
return mock.PutBucketAclFunc(putBucketAclInput)
|
||||
return mock.PutBucketAclFunc(bucket, data)
|
||||
}
|
||||
|
||||
// PutBucketAclCalls gets all the calls that were made to PutBucketAcl.
|
||||
@@ -1313,10 +1316,12 @@ func (mock *BackendMock) PutBucketAcl(putBucketAclInput *s3.PutBucketAclInput) e
|
||||
//
|
||||
// len(mockedBackend.PutBucketAclCalls())
|
||||
func (mock *BackendMock) PutBucketAclCalls() []struct {
|
||||
PutBucketAclInput *s3.PutBucketAclInput
|
||||
Bucket string
|
||||
Data []byte
|
||||
} {
|
||||
var calls []struct {
|
||||
PutBucketAclInput *s3.PutBucketAclInput
|
||||
Bucket string
|
||||
Data []byte
|
||||
}
|
||||
mock.lockPutBucketAcl.RLock()
|
||||
calls = mock.calls.PutBucketAcl
|
||||
|
||||
@@ -29,24 +29,24 @@ 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/versity/versitygw/auth"
|
||||
"github.com/versity/versitygw/backend"
|
||||
"github.com/versity/versitygw/backend/auth"
|
||||
"github.com/versity/versitygw/s3api/utils"
|
||||
"github.com/versity/versitygw/s3err"
|
||||
)
|
||||
|
||||
type S3ApiController struct {
|
||||
be backend.Backend
|
||||
acl auth.ACLService
|
||||
iam auth.IAMService
|
||||
}
|
||||
|
||||
func New(be backend.Backend) S3ApiController {
|
||||
return S3ApiController{be: be, acl: auth.ACLServiceUnsupported{}}
|
||||
return S3ApiController{be: be}
|
||||
}
|
||||
|
||||
func (c S3ApiController) ListBuckets(ctx *fiber.Ctx) error {
|
||||
access, isRoot := ctx.Locals("access").(string), ctx.Locals("isRoot").(bool)
|
||||
if err := c.acl.IsAdmin(access, isRoot); err != nil {
|
||||
if err := auth.IsAdmin(access, isRoot); err != nil {
|
||||
return SendXMLResponse(ctx, nil, err)
|
||||
}
|
||||
res, err := c.be.ListBuckets()
|
||||
@@ -67,6 +67,16 @@ func (c S3ApiController) GetActions(ctx *fiber.Ctx) error {
|
||||
key = strings.Join([]string{key, keyEnd}, "/")
|
||||
}
|
||||
|
||||
data, err := c.be.GetBucketAcl(bucket)
|
||||
if err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
}
|
||||
|
||||
parsedAcl, err := auth.ParseACL(data)
|
||||
if err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
}
|
||||
|
||||
if uploadId != "" {
|
||||
if maxParts < 0 || (maxParts == 0 && ctx.Query("max-parts") != "") {
|
||||
return SendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidMaxParts))
|
||||
@@ -75,7 +85,7 @@ func (c S3ApiController) GetActions(ctx *fiber.Ctx) error {
|
||||
return SendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidPartNumberMarker))
|
||||
}
|
||||
|
||||
if err := c.acl.VerifyACL(bucket, access, "READ", isRoot); err != nil {
|
||||
if err := auth.VerifyACL(parsedAcl, bucket, access, "READ", isRoot); err != nil {
|
||||
return SendXMLResponse(ctx, nil, err)
|
||||
}
|
||||
|
||||
@@ -84,7 +94,7 @@ func (c S3ApiController) GetActions(ctx *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
if ctx.Request().URI().QueryArgs().Has("acl") {
|
||||
if err := c.acl.VerifyACL(bucket, access, "READ_ACP", isRoot); err != nil {
|
||||
if err := auth.VerifyACL(parsedAcl, bucket, access, "READ_ACP", isRoot); err != nil {
|
||||
return SendXMLResponse(ctx, nil, err)
|
||||
}
|
||||
res, err := c.be.GetObjectAcl(bucket, key)
|
||||
@@ -92,14 +102,14 @@ func (c S3ApiController) GetActions(ctx *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
if attrs := ctx.Get("X-Amz-Object-Attributes"); attrs != "" {
|
||||
if err := c.acl.VerifyACL(bucket, access, "READ", isRoot); err != nil {
|
||||
if err := auth.VerifyACL(parsedAcl, bucket, access, "READ", isRoot); err != nil {
|
||||
return SendXMLResponse(ctx, nil, err)
|
||||
}
|
||||
res, err := c.be.GetObjectAttributes(bucket, key, strings.Split(attrs, ","))
|
||||
return SendXMLResponse(ctx, res, err)
|
||||
}
|
||||
|
||||
if err := c.acl.VerifyACL(bucket, access, "READ_ACP", isRoot); err != nil {
|
||||
if err := auth.VerifyACL(parsedAcl, bucket, access, "READ_ACP", isRoot); err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
}
|
||||
|
||||
@@ -157,16 +167,27 @@ func (c S3ApiController) ListActions(ctx *fiber.Ctx) error {
|
||||
access := ctx.Locals("access").(string)
|
||||
isRoot := ctx.Locals("isRoot").(bool)
|
||||
|
||||
data, err := c.be.GetBucketAcl(bucket)
|
||||
if err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
}
|
||||
|
||||
parsedAcl, err := auth.ParseACL(data)
|
||||
if err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
}
|
||||
|
||||
if ctx.Request().URI().QueryArgs().Has("acl") {
|
||||
if err := c.acl.VerifyACL(bucket, access, "READ_ACP", isRoot); err != nil {
|
||||
if err := auth.VerifyACL(parsedAcl, bucket, access, "READ_ACP", isRoot); err != nil {
|
||||
return SendXMLResponse(ctx, nil, err)
|
||||
}
|
||||
res, err := c.be.GetBucketAcl(ctx.Params("bucket"))
|
||||
|
||||
res, err := auth.ParseACLOutput(data)
|
||||
return SendXMLResponse(ctx, res, err)
|
||||
}
|
||||
|
||||
if ctx.Request().URI().QueryArgs().Has("uploads") {
|
||||
if err := c.acl.VerifyACL(bucket, access, "READ", isRoot); err != nil {
|
||||
if err := auth.VerifyACL(parsedAcl, bucket, access, "READ", isRoot); err != nil {
|
||||
return SendXMLResponse(ctx, nil, err)
|
||||
}
|
||||
res, err := c.be.ListMultipartUploads(&s3.ListMultipartUploadsInput{Bucket: aws.String(ctx.Params("bucket"))})
|
||||
@@ -174,14 +195,14 @@ func (c S3ApiController) ListActions(ctx *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
if ctx.QueryInt("list-type") == 2 {
|
||||
if err := c.acl.VerifyACL(bucket, access, "READ", isRoot); err != nil {
|
||||
if err := auth.VerifyACL(parsedAcl, bucket, access, "READ", isRoot); err != nil {
|
||||
return SendXMLResponse(ctx, nil, err)
|
||||
}
|
||||
res, err := c.be.ListObjectsV2(bucket, prefix, marker, delimiter, maxkeys)
|
||||
return SendXMLResponse(ctx, res, err)
|
||||
}
|
||||
|
||||
if err := c.acl.VerifyACL(bucket, access, "READ", isRoot); err != nil {
|
||||
if err := auth.VerifyACL(parsedAcl, bucket, access, "READ", isRoot); err != nil {
|
||||
return SendXMLResponse(ctx, nil, err)
|
||||
}
|
||||
|
||||
@@ -212,11 +233,21 @@ func (c S3ApiController) PutBucketActions(ctx *fiber.Ctx) error {
|
||||
return errors.New("wrong api call")
|
||||
}
|
||||
|
||||
if err := c.acl.VerifyACL(bucket, access, "WRITE_ACP", isRoot); err != nil {
|
||||
data, err := c.be.GetBucketAcl(bucket)
|
||||
if err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
}
|
||||
|
||||
err := c.be.PutBucketAcl(&s3.PutBucketAclInput{
|
||||
parsedAcl, err := auth.ParseACL(data)
|
||||
if err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
}
|
||||
|
||||
if err := auth.VerifyACL(parsedAcl, bucket, access, "WRITE_ACP", isRoot); err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
}
|
||||
|
||||
input := &s3.PutBucketAclInput{
|
||||
Bucket: &bucket,
|
||||
ACL: types.BucketCannedACL(acl),
|
||||
GrantFullControl: &grantFullControl,
|
||||
@@ -225,8 +256,9 @@ func (c S3ApiController) PutBucketActions(ctx *fiber.Ctx) error {
|
||||
GrantWrite: &granWrite,
|
||||
GrantWriteACP: &grantWriteACP,
|
||||
AccessControlPolicy: &types.AccessControlPolicy{Owner: &types.Owner{ID: &access}},
|
||||
})
|
||||
}
|
||||
|
||||
err = auth.UpdateACL(input, parsedAcl, c.iam)
|
||||
return SendResponse(ctx, err)
|
||||
}
|
||||
|
||||
@@ -280,13 +312,23 @@ func (c S3ApiController) PutActions(ctx *fiber.Ctx) error {
|
||||
}
|
||||
}
|
||||
|
||||
data, err := c.be.GetBucketAcl(bucket)
|
||||
if err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
}
|
||||
|
||||
parsedAcl, err := auth.ParseACL(data)
|
||||
if err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
}
|
||||
|
||||
if uploadId != "" && partNumberStr != "" {
|
||||
partNumber := ctx.QueryInt("partNumber", -1)
|
||||
if partNumber < 1 {
|
||||
return SendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidPart))
|
||||
}
|
||||
|
||||
if err := c.acl.VerifyACL(bucket, access, "WRITE", isRoot); err != nil {
|
||||
if err := auth.VerifyACL(parsedAcl, bucket, access, "WRITE", isRoot); err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
}
|
||||
|
||||
@@ -302,7 +344,7 @@ func (c S3ApiController) PutActions(ctx *fiber.Ctx) error {
|
||||
return errors.New("wrong api call")
|
||||
}
|
||||
|
||||
if err := c.acl.VerifyACL(bucket, access, "WRITE_ACP", isRoot); err != nil {
|
||||
if err := auth.VerifyACL(parsedAcl, bucket, access, "WRITE_ACP", isRoot); err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
}
|
||||
|
||||
@@ -325,7 +367,7 @@ func (c S3ApiController) PutActions(ctx *fiber.Ctx) error {
|
||||
copySourceSplit := strings.Split(copySource, "/")
|
||||
srcBucket, srcObject := copySourceSplit[0], copySourceSplit[1:]
|
||||
|
||||
if err := c.acl.VerifyACL(bucket, access, "WRITE", isRoot); err != nil {
|
||||
if err := auth.VerifyACL(parsedAcl, bucket, access, "WRITE", isRoot); err != nil {
|
||||
return SendXMLResponse(ctx, nil, err)
|
||||
}
|
||||
|
||||
@@ -335,7 +377,7 @@ func (c S3ApiController) PutActions(ctx *fiber.Ctx) error {
|
||||
|
||||
metadata := utils.GetUserMetaData(&ctx.Request().Header)
|
||||
|
||||
if err := c.acl.VerifyACL(bucket, access, "WRITE", isRoot); err != nil {
|
||||
if err := auth.VerifyACL(parsedAcl, bucket, access, "WRITE", isRoot); err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
}
|
||||
|
||||
@@ -352,10 +394,22 @@ func (c S3ApiController) PutActions(ctx *fiber.Ctx) error {
|
||||
|
||||
func (c S3ApiController) DeleteBucket(ctx *fiber.Ctx) error {
|
||||
bucket, access, isRoot := ctx.Params("bucket"), ctx.Locals("access").(string), ctx.Locals("isRoot").(bool)
|
||||
if err := c.acl.VerifyACL(bucket, access, "WRITE", isRoot); err != nil {
|
||||
|
||||
data, err := c.be.GetBucketAcl(bucket)
|
||||
if err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
}
|
||||
err := c.be.DeleteBucket(bucket)
|
||||
|
||||
parsedAcl, err := auth.ParseACL(data)
|
||||
if err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
}
|
||||
|
||||
if err := auth.VerifyACL(parsedAcl, bucket, access, "WRITE", isRoot); err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
}
|
||||
|
||||
err = c.be.DeleteBucket(bucket)
|
||||
return SendResponse(ctx, err)
|
||||
}
|
||||
|
||||
@@ -367,11 +421,21 @@ func (c S3ApiController) DeleteObjects(ctx *fiber.Ctx) error {
|
||||
return errors.New("wrong api call")
|
||||
}
|
||||
|
||||
if err := c.acl.VerifyACL(bucket, access, "WRITE", isRoot); err != nil {
|
||||
data, err := c.be.GetBucketAcl(bucket)
|
||||
if err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
}
|
||||
|
||||
err := c.be.DeleteObjects(bucket, &s3.DeleteObjectsInput{Delete: &dObj})
|
||||
parsedAcl, err := auth.ParseACL(data)
|
||||
if err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
}
|
||||
|
||||
if err := auth.VerifyACL(parsedAcl, bucket, access, "WRITE", isRoot); err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
}
|
||||
|
||||
err = c.be.DeleteObjects(bucket, &s3.DeleteObjectsInput{Delete: &dObj})
|
||||
return SendResponse(ctx, err)
|
||||
}
|
||||
|
||||
@@ -387,10 +451,20 @@ func (c S3ApiController) DeleteActions(ctx *fiber.Ctx) error {
|
||||
key = strings.Join([]string{key, keyEnd}, "/")
|
||||
}
|
||||
|
||||
data, err := c.be.GetBucketAcl(bucket)
|
||||
if err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
}
|
||||
|
||||
parsedAcl, err := auth.ParseACL(data)
|
||||
if err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
}
|
||||
|
||||
if uploadId != "" {
|
||||
expectedBucketOwner, requestPayer := ctx.Get("X-Amz-Expected-Bucket-Owner"), ctx.Get("X-Amz-Request-Payer")
|
||||
|
||||
if err := c.acl.VerifyACL(bucket, access, "WRITE", isRoot); err != nil {
|
||||
if err := auth.VerifyACL(parsedAcl, bucket, access, "WRITE", isRoot); err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
}
|
||||
|
||||
@@ -404,21 +478,32 @@ func (c S3ApiController) DeleteActions(ctx *fiber.Ctx) error {
|
||||
return SendResponse(ctx, err)
|
||||
}
|
||||
|
||||
if err := c.acl.VerifyACL(bucket, access, "WRITE", isRoot); err != nil {
|
||||
if err := auth.VerifyACL(parsedAcl, bucket, access, "WRITE", isRoot); err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
}
|
||||
|
||||
err := c.be.DeleteObject(bucket, key)
|
||||
err = c.be.DeleteObject(bucket, key)
|
||||
return SendResponse(ctx, err)
|
||||
}
|
||||
|
||||
func (c S3ApiController) HeadBucket(ctx *fiber.Ctx) error {
|
||||
bucket, access, isRoot := ctx.Params("bucket"), ctx.Locals("access").(string), ctx.Locals("isRoot").(bool)
|
||||
if err := c.acl.VerifyACL(bucket, access, "READ", isRoot); err != nil {
|
||||
|
||||
data, err := c.be.GetBucketAcl(bucket)
|
||||
if err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
}
|
||||
|
||||
_, err := c.be.HeadBucket(bucket)
|
||||
parsedAcl, err := auth.ParseACL(data)
|
||||
if err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
}
|
||||
|
||||
if err := auth.VerifyACL(parsedAcl, bucket, access, "READ", isRoot); err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
}
|
||||
|
||||
_, err = c.be.HeadBucket(bucket)
|
||||
// TODO: set bucket response headers
|
||||
return SendResponse(ctx, err)
|
||||
}
|
||||
@@ -435,7 +520,17 @@ func (c S3ApiController) HeadObject(ctx *fiber.Ctx) error {
|
||||
key = strings.Join([]string{key, keyEnd}, "/")
|
||||
}
|
||||
|
||||
if err := c.acl.VerifyACL(bucket, access, "READ", isRoot); err != nil {
|
||||
data, err := c.be.GetBucketAcl(bucket)
|
||||
if err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
}
|
||||
|
||||
parsedAcl, err := auth.ParseACL(data)
|
||||
if err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
}
|
||||
|
||||
if err := auth.VerifyACL(parsedAcl, bucket, access, "READ", isRoot); err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
}
|
||||
|
||||
@@ -490,6 +585,16 @@ func (c S3ApiController) CreateActions(ctx *fiber.Ctx) error {
|
||||
key = strings.Join([]string{key, keyEnd}, "/")
|
||||
}
|
||||
|
||||
data, err := c.be.GetBucketAcl(bucket)
|
||||
if err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
}
|
||||
|
||||
parsedAcl, err := auth.ParseACL(data)
|
||||
if err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
}
|
||||
|
||||
var restoreRequest s3.RestoreObjectInput
|
||||
if ctx.Request().URI().QueryArgs().Has("restore") {
|
||||
xmlErr := xml.Unmarshal(ctx.Body(), &restoreRequest)
|
||||
@@ -497,7 +602,7 @@ func (c S3ApiController) CreateActions(ctx *fiber.Ctx) error {
|
||||
return errors.New("wrong api call")
|
||||
}
|
||||
|
||||
if err := c.acl.VerifyACL(bucket, access, "WRITE", isRoot); err != nil {
|
||||
if err := auth.VerifyACL(parsedAcl, bucket, access, "WRITE", isRoot); err != nil {
|
||||
return SendResponse(ctx, err)
|
||||
}
|
||||
|
||||
@@ -514,7 +619,7 @@ func (c S3ApiController) CreateActions(ctx *fiber.Ctx) error {
|
||||
return errors.New("wrong api call")
|
||||
}
|
||||
|
||||
if err := c.acl.VerifyACL(bucket, access, "WRITE", isRoot); err != nil {
|
||||
if err := auth.VerifyACL(parsedAcl, bucket, access, "WRITE", isRoot); err != nil {
|
||||
return SendXMLResponse(ctx, nil, err)
|
||||
}
|
||||
|
||||
@@ -522,7 +627,7 @@ func (c S3ApiController) CreateActions(ctx *fiber.Ctx) error {
|
||||
return SendXMLResponse(ctx, res, err)
|
||||
}
|
||||
|
||||
if err := c.acl.VerifyACL(bucket, access, "WRITE", isRoot); err != nil {
|
||||
if err := auth.VerifyACL(parsedAcl, bucket, access, "WRITE", isRoot); err != nil {
|
||||
return SendXMLResponse(ctx, nil, err)
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -26,19 +27,31 @@ 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/versity/versitygw/auth"
|
||||
"github.com/versity/versitygw/backend"
|
||||
"github.com/versity/versitygw/backend/auth"
|
||||
"github.com/versity/versitygw/s3err"
|
||||
"github.com/versity/versitygw/s3response"
|
||||
)
|
||||
|
||||
var (
|
||||
acl auth.ACL
|
||||
acldata []byte
|
||||
)
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
acldata, err = json.Marshal(acl)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
type args struct {
|
||||
be backend.Backend
|
||||
}
|
||||
|
||||
be := backend.BackendUnsupported{}
|
||||
acl := auth.ACLServiceUnsupported{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -51,8 +64,7 @@ func TestNew(t *testing.T) {
|
||||
be: be,
|
||||
},
|
||||
want: S3ApiController{
|
||||
be: be,
|
||||
acl: acl,
|
||||
be: be,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -73,11 +85,13 @@ 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) {
|
||||
return s3response.ListAllMyBucketsResult{}, nil
|
||||
},
|
||||
},
|
||||
acl: auth.ACLServiceUnsupported{},
|
||||
}
|
||||
|
||||
app.Use(func(ctx *fiber.Ctx) error {
|
||||
@@ -91,11 +105,13 @@ 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) {
|
||||
return s3response.ListAllMyBucketsResult{}, s3err.GetAPIError(s3err.ErrMethodNotAllowed)
|
||||
},
|
||||
},
|
||||
acl: auth.ACLServiceUnsupported{},
|
||||
}
|
||||
|
||||
appErr.Use(func(ctx *fiber.Ctx) error {
|
||||
@@ -154,6 +170,9 @@ func TestS3ApiController_GetActions(t *testing.T) {
|
||||
app := fiber.New()
|
||||
s3ApiController := S3ApiController{
|
||||
be: &BackendMock{
|
||||
GetBucketAclFunc: func(bucket string) ([]byte, error) {
|
||||
return acldata, nil
|
||||
},
|
||||
ListObjectPartsFunc: func(bucket, object, uploadID string, partNumberMarker int, maxParts int) (s3response.ListPartsResponse, error) {
|
||||
return s3response.ListPartsResponse{}, nil
|
||||
},
|
||||
@@ -167,7 +186,6 @@ func TestS3ApiController_GetActions(t *testing.T) {
|
||||
return &s3.GetObjectOutput{Metadata: nil}, nil
|
||||
},
|
||||
},
|
||||
acl: &auth.ACLServiceUnsupported{},
|
||||
}
|
||||
app.Use(func(ctx *fiber.Ctx) error {
|
||||
ctx.Locals("access", "valid access")
|
||||
@@ -264,8 +282,8 @@ func TestS3ApiController_ListActions(t *testing.T) {
|
||||
app := fiber.New()
|
||||
s3ApiController := S3ApiController{
|
||||
be: &BackendMock{
|
||||
GetBucketAclFunc: func(bucket string) (*auth.GetBucketAclOutput, error) {
|
||||
return nil, nil
|
||||
GetBucketAclFunc: func(bucket string) ([]byte, error) {
|
||||
return acldata, nil
|
||||
},
|
||||
ListMultipartUploadsFunc: func(output *s3.ListMultipartUploadsInput) (s3response.ListMultipartUploadsResponse, error) {
|
||||
return s3response.ListMultipartUploadsResponse{}, nil
|
||||
@@ -277,7 +295,6 @@ func TestS3ApiController_ListActions(t *testing.T) {
|
||||
return &s3.ListObjectsOutput{}, nil
|
||||
},
|
||||
},
|
||||
acl: auth.ACLServiceUnsupported{},
|
||||
}
|
||||
|
||||
app.Use(func(ctx *fiber.Ctx) error {
|
||||
@@ -291,11 +308,13 @@ func TestS3ApiController_ListActions(t *testing.T) {
|
||||
//Error case
|
||||
s3ApiControllerError := S3ApiController{
|
||||
be: &BackendMock{
|
||||
GetBucketAclFunc: func(bucket string) ([]byte, error) {
|
||||
return acldata, nil
|
||||
},
|
||||
ListObjectsFunc: func(bucket, prefix, marker, delim string, maxkeys int) (*s3.ListObjectsOutput, error) {
|
||||
return nil, s3err.GetAPIError(s3err.ErrNotImplemented)
|
||||
},
|
||||
},
|
||||
acl: auth.ACLServiceUnsupported{},
|
||||
}
|
||||
appError := fiber.New()
|
||||
appError.Use(func(ctx *fiber.Ctx) error {
|
||||
@@ -381,14 +400,16 @@ func TestS3ApiController_PutBucketActions(t *testing.T) {
|
||||
app := fiber.New()
|
||||
s3ApiController := S3ApiController{
|
||||
be: &BackendMock{
|
||||
PutBucketAclFunc: func(*s3.PutBucketAclInput) error {
|
||||
GetBucketAclFunc: func(bucket string) ([]byte, error) {
|
||||
return acldata, nil
|
||||
},
|
||||
PutBucketAclFunc: func(string, []byte) error {
|
||||
return nil
|
||||
},
|
||||
PutBucketFunc: func(bucket, owner string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
acl: auth.ACLServiceUnsupported{},
|
||||
}
|
||||
// Mock ctx.Locals
|
||||
app.Use(func(ctx *fiber.Ctx) error {
|
||||
@@ -463,6 +484,9 @@ func TestS3ApiController_PutActions(t *testing.T) {
|
||||
app := fiber.New()
|
||||
s3ApiController := S3ApiController{
|
||||
be: &BackendMock{
|
||||
GetBucketAclFunc: func(bucket string) ([]byte, error) {
|
||||
return acldata, nil
|
||||
},
|
||||
UploadPartCopyFunc: func(*s3.UploadPartCopyInput) (*s3.UploadPartCopyOutput, error) {
|
||||
return &s3.UploadPartCopyOutput{}, nil
|
||||
},
|
||||
@@ -476,7 +500,6 @@ func TestS3ApiController_PutActions(t *testing.T) {
|
||||
return "Hey", nil
|
||||
},
|
||||
},
|
||||
acl: auth.ACLServiceUnsupported{},
|
||||
}
|
||||
app.Use(func(ctx *fiber.Ctx) error {
|
||||
ctx.Locals("access", "valid access")
|
||||
@@ -601,11 +624,13 @@ func TestS3ApiController_DeleteBucket(t *testing.T) {
|
||||
app := fiber.New()
|
||||
s3ApiController := S3ApiController{
|
||||
be: &BackendMock{
|
||||
GetBucketAclFunc: func(bucket string) ([]byte, error) {
|
||||
return acldata, nil
|
||||
},
|
||||
DeleteBucketFunc: func(bucket string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
acl: auth.ACLServiceUnsupported{},
|
||||
}
|
||||
|
||||
app.Use(func(ctx *fiber.Ctx) error {
|
||||
@@ -621,11 +646,13 @@ func TestS3ApiController_DeleteBucket(t *testing.T) {
|
||||
|
||||
s3ApiControllerErr := S3ApiController{
|
||||
be: &BackendMock{
|
||||
GetBucketAclFunc: func(bucket string) ([]byte, error) {
|
||||
return acldata, nil
|
||||
},
|
||||
DeleteBucketFunc: func(bucket string) error {
|
||||
return s3err.GetAPIError(48)
|
||||
},
|
||||
},
|
||||
acl: auth.ACLServiceUnsupported{},
|
||||
}
|
||||
|
||||
appErr.Use(func(ctx *fiber.Ctx) error {
|
||||
@@ -682,11 +709,13 @@ func TestS3ApiController_DeleteObjects(t *testing.T) {
|
||||
app := fiber.New()
|
||||
s3ApiController := S3ApiController{
|
||||
be: &BackendMock{
|
||||
GetBucketAclFunc: func(bucket string) ([]byte, error) {
|
||||
return acldata, nil
|
||||
},
|
||||
DeleteObjectsFunc: func(bucket string, objects *s3.DeleteObjectsInput) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
acl: auth.ACLServiceUnsupported{},
|
||||
}
|
||||
|
||||
app.Use(func(ctx *fiber.Ctx) error {
|
||||
@@ -749,6 +778,9 @@ func TestS3ApiController_DeleteActions(t *testing.T) {
|
||||
app := fiber.New()
|
||||
s3ApiController := S3ApiController{
|
||||
be: &BackendMock{
|
||||
GetBucketAclFunc: func(bucket string) ([]byte, error) {
|
||||
return acldata, nil
|
||||
},
|
||||
DeleteObjectFunc: func(bucket, object string) error {
|
||||
return nil
|
||||
},
|
||||
@@ -756,7 +788,6 @@ func TestS3ApiController_DeleteActions(t *testing.T) {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
acl: auth.ACLServiceUnsupported{},
|
||||
}
|
||||
|
||||
app.Use(func(ctx *fiber.Ctx) error {
|
||||
@@ -770,6 +801,9 @@ func TestS3ApiController_DeleteActions(t *testing.T) {
|
||||
appErr := fiber.New()
|
||||
|
||||
s3ApiControllerErr := S3ApiController{be: &BackendMock{
|
||||
GetBucketAclFunc: func(bucket string) ([]byte, error) {
|
||||
return acldata, nil
|
||||
},
|
||||
DeleteObjectFunc: func(bucket, object string) error {
|
||||
return s3err.GetAPIError(7)
|
||||
},
|
||||
@@ -838,11 +872,13 @@ func TestS3ApiController_HeadBucket(t *testing.T) {
|
||||
app := fiber.New()
|
||||
s3ApiController := S3ApiController{
|
||||
be: &BackendMock{
|
||||
GetBucketAclFunc: func(bucket string) ([]byte, error) {
|
||||
return acldata, nil
|
||||
},
|
||||
HeadBucketFunc: func(bucket string) (*s3.HeadBucketOutput, error) {
|
||||
return &s3.HeadBucketOutput{}, nil
|
||||
},
|
||||
},
|
||||
acl: auth.ACLServiceUnsupported{},
|
||||
}
|
||||
|
||||
app.Use(func(ctx *fiber.Ctx) error {
|
||||
@@ -857,11 +893,13 @@ func TestS3ApiController_HeadBucket(t *testing.T) {
|
||||
appErr := fiber.New()
|
||||
|
||||
s3ApiControllerErr := S3ApiController{be: &BackendMock{
|
||||
GetBucketAclFunc: func(bucket string) ([]byte, error) {
|
||||
return acldata, nil
|
||||
},
|
||||
HeadBucketFunc: func(bucket string) (*s3.HeadBucketOutput, error) {
|
||||
return nil, s3err.GetAPIError(3)
|
||||
},
|
||||
},
|
||||
acl: auth.ACLServiceUnsupported{},
|
||||
}
|
||||
|
||||
appErr.Use(func(ctx *fiber.Ctx) error {
|
||||
@@ -926,6 +964,9 @@ func TestS3ApiController_HeadObject(t *testing.T) {
|
||||
|
||||
s3ApiController := S3ApiController{
|
||||
be: &BackendMock{
|
||||
GetBucketAclFunc: func(bucket string) ([]byte, error) {
|
||||
return acldata, nil
|
||||
},
|
||||
HeadObjectFunc: func(bucket, object string) (*s3.HeadObjectOutput, error) {
|
||||
return &s3.HeadObjectOutput{
|
||||
ContentEncoding: &contentEncoding,
|
||||
@@ -936,7 +977,6 @@ func TestS3ApiController_HeadObject(t *testing.T) {
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
acl: auth.ACLServiceUnsupported{},
|
||||
}
|
||||
|
||||
app.Use(func(ctx *fiber.Ctx) error {
|
||||
@@ -951,11 +991,13 @@ func TestS3ApiController_HeadObject(t *testing.T) {
|
||||
|
||||
s3ApiControllerErr := S3ApiController{
|
||||
be: &BackendMock{
|
||||
GetBucketAclFunc: func(bucket string) ([]byte, error) {
|
||||
return acldata, nil
|
||||
},
|
||||
HeadObjectFunc: func(bucket, object string) (*s3.HeadObjectOutput, error) {
|
||||
return nil, s3err.GetAPIError(42)
|
||||
},
|
||||
},
|
||||
acl: auth.ACLServiceUnsupported{},
|
||||
}
|
||||
|
||||
appErr.Use(func(ctx *fiber.Ctx) error {
|
||||
@@ -1011,6 +1053,9 @@ func TestS3ApiController_CreateActions(t *testing.T) {
|
||||
app := fiber.New()
|
||||
s3ApiController := S3ApiController{
|
||||
be: &BackendMock{
|
||||
GetBucketAclFunc: func(bucket string) ([]byte, error) {
|
||||
return acldata, nil
|
||||
},
|
||||
RestoreObjectFunc: func(bucket, object string, restoreRequest *s3.RestoreObjectInput) error {
|
||||
return nil
|
||||
},
|
||||
@@ -1021,7 +1066,6 @@ func TestS3ApiController_CreateActions(t *testing.T) {
|
||||
return &s3.CreateMultipartUploadOutput{}, nil
|
||||
},
|
||||
},
|
||||
acl: auth.ACLServiceUnsupported{},
|
||||
}
|
||||
|
||||
app.Use(func(ctx *fiber.Ctx) error {
|
||||
|
||||
@@ -25,7 +25,7 @@ import (
|
||||
v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
|
||||
"github.com/aws/smithy-go/logging"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/versity/versitygw/backend/auth"
|
||||
"github.com/versity/versitygw/auth"
|
||||
"github.com/versity/versitygw/s3api/controllers"
|
||||
"github.com/versity/versitygw/s3api/utils"
|
||||
"github.com/versity/versitygw/s3err"
|
||||
@@ -38,10 +38,9 @@ const (
|
||||
type RootUserConfig struct {
|
||||
Access string
|
||||
Secret string
|
||||
Region string
|
||||
}
|
||||
|
||||
func VerifyV4Signature(root RootUserConfig, iam auth.IAMService, debug bool) fiber.Handler {
|
||||
func VerifyV4Signature(root RootUserConfig, iam auth.IAMService, region string, debug bool) fiber.Handler {
|
||||
acct := accounts{root: root, iam: iam}
|
||||
|
||||
return func(ctx *fiber.Ctx) error {
|
||||
@@ -74,10 +73,13 @@ func VerifyV4Signature(root RootUserConfig, iam auth.IAMService, debug bool) fib
|
||||
}
|
||||
signedHdrs := strings.Split(signHdrKv[1], ";")
|
||||
|
||||
account := acct.getAccount(creds[0])
|
||||
if account == nil {
|
||||
account, err := acct.getAccount(creds[0])
|
||||
if err == auth.ErrNoSuchUser {
|
||||
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidAccessKeyID))
|
||||
}
|
||||
if err != nil {
|
||||
return controllers.SendResponse(ctx, err)
|
||||
}
|
||||
|
||||
// Check X-Amz-Date header
|
||||
date := ctx.Get("X-Amz-Date")
|
||||
@@ -113,7 +115,7 @@ func VerifyV4Signature(root RootUserConfig, iam auth.IAMService, debug bool) fib
|
||||
signErr := signer.SignHTTP(req.Context(), aws.Credentials{
|
||||
AccessKeyID: creds[0],
|
||||
SecretAccessKey: account.Secret,
|
||||
}, req, hexPayload, creds[3], account.Region, tdate, func(options *v4.SignerOptions) {
|
||||
}, req, hexPayload, creds[3], region, tdate, func(options *v4.SignerOptions) {
|
||||
if debug {
|
||||
options.LogSigning = true
|
||||
options.Logger = logging.NewStandardLogger(os.Stderr)
|
||||
@@ -147,16 +149,13 @@ type accounts struct {
|
||||
iam auth.IAMService
|
||||
}
|
||||
|
||||
func (a accounts) getAccount(access string) *auth.Account {
|
||||
var account *auth.Account
|
||||
func (a accounts) getAccount(access string) (auth.Account, error) {
|
||||
if access == a.root.Access {
|
||||
account = &auth.Account{
|
||||
return auth.Account{
|
||||
Secret: a.root.Secret,
|
||||
Role: "admin",
|
||||
Region: a.root.Region,
|
||||
}
|
||||
} else {
|
||||
account = a.iam.GetUserAccount(access)
|
||||
}, nil
|
||||
}
|
||||
return account
|
||||
|
||||
return a.iam.GetUserAccount(access)
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ package s3api
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/versity/versitygw/auth"
|
||||
"github.com/versity/versitygw/backend"
|
||||
"github.com/versity/versitygw/backend/auth"
|
||||
"github.com/versity/versitygw/s3api/controllers"
|
||||
)
|
||||
|
||||
|
||||
@@ -18,8 +18,8 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/versity/versitygw/auth"
|
||||
"github.com/versity/versitygw/backend"
|
||||
"github.com/versity/versitygw/backend/auth"
|
||||
)
|
||||
|
||||
func TestS3ApiRouter_Init(t *testing.T) {
|
||||
@@ -39,7 +39,7 @@ func TestS3ApiRouter_Init(t *testing.T) {
|
||||
args: args{
|
||||
app: fiber.New(),
|
||||
be: backend.BackendUnsupported{},
|
||||
iam: auth.IAMServiceUnsupported{},
|
||||
iam: &auth.IAMServiceInternal{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -19,8 +19,8 @@ import (
|
||||
|
||||
"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/backend/auth"
|
||||
"github.com/versity/versitygw/s3api/middlewares"
|
||||
)
|
||||
|
||||
@@ -33,7 +33,7 @@ type S3ApiServer struct {
|
||||
debug bool
|
||||
}
|
||||
|
||||
func New(app *fiber.App, be backend.Backend, root middlewares.RootUserConfig, port 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, opts ...Option) (*S3ApiServer, error) {
|
||||
server := &S3ApiServer{
|
||||
app: app,
|
||||
backend: be,
|
||||
@@ -50,7 +50,7 @@ func New(app *fiber.App, be backend.Backend, root middlewares.RootUserConfig, po
|
||||
app.Use(middlewares.RequestLogger)
|
||||
|
||||
// Authentication middlewares
|
||||
app.Use(middlewares.VerifyV4Signature(root, iam, server.debug))
|
||||
app.Use(middlewares.VerifyV4Signature(root, iam, region, server.debug))
|
||||
app.Use(middlewares.VerifyMD5Body())
|
||||
|
||||
server.router.Init(app, be, iam)
|
||||
|
||||
@@ -19,8 +19,8 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/versity/versitygw/auth"
|
||||
"github.com/versity/versitygw/backend"
|
||||
"github.com/versity/versitygw/backend/auth"
|
||||
"github.com/versity/versitygw/s3api/middlewares"
|
||||
)
|
||||
|
||||
@@ -63,7 +63,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, auth.IAMServiceUnsupported{})
|
||||
tt.args.port, "us-east-1", &auth.IAMServiceInternal{})
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user