seaweedfs/weed/s3api/auth_credentials.go
Patrick Schmidt cdd817edf9 Improve S3 request signing performance
This change is caching HMAC hashers for repeated use in subsequent
requests and chunks, so they don't have to be initialized from
scratch every time.
On my local computer this gives me ~5-6 times faster signature
calculation and ~5-6.5% more throughput in S3 requests. The smaller
the payload the better the throughput gets.
2023-09-05 10:33:27 -07:00

397 lines
11 KiB
Go

package s3api
import (
"fmt"
"net/http"
"os"
"strings"
"sync"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3account"
"github.com/seaweedfs/seaweedfs/weed/filer"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/pb"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
)
var IdentityAnonymous *Identity
type Action string
type Iam interface {
Check(f http.HandlerFunc, actions ...Action) http.HandlerFunc
}
type IdentityAccessManagement struct {
m sync.RWMutex
identities []*Identity
isAuthEnabled bool
domain string
hashes map[string]*sync.Pool
hashMu sync.RWMutex
}
type Identity struct {
Name string
AccountId string
Credentials []*Credential
Actions []Action
}
func (i *Identity) isAnonymous() bool {
return i.Name == s3account.AccountAnonymous.Name
}
type Credential struct {
AccessKey string
SecretKey string
}
func (action Action) isAdmin() bool {
return strings.HasPrefix(string(action), s3_constants.ACTION_ADMIN)
}
func (action Action) isOwner(bucket string) bool {
return string(action) == s3_constants.ACTION_ADMIN+":"+bucket
}
func (action Action) overBucket(bucket string) bool {
return strings.HasSuffix(string(action), ":"+bucket) || strings.HasSuffix(string(action), ":*")
}
func (action Action) getPermission() Permission {
switch act := strings.Split(string(action), ":")[0]; act {
case s3_constants.ACTION_ADMIN:
return Permission("FULL_CONTROL")
case s3_constants.ACTION_WRITE:
return Permission("WRITE")
case s3_constants.ACTION_READ:
return Permission("READ")
default:
return Permission("")
}
}
func NewIdentityAccessManagement(option *S3ApiServerOption) *IdentityAccessManagement {
iam := &IdentityAccessManagement{
domain: option.DomainName,
hashes: make(map[string]*sync.Pool),
}
if option.Config != "" {
if err := iam.loadS3ApiConfigurationFromFile(option.Config); err != nil {
glog.Fatalf("fail to load config file %s: %v", option.Config, err)
}
} else {
if err := iam.loadS3ApiConfigurationFromFiler(option); err != nil {
glog.Warningf("fail to load config: %v", err)
}
}
return iam
}
func (iam *IdentityAccessManagement) loadS3ApiConfigurationFromFiler(option *S3ApiServerOption) (err error) {
var content []byte
err = pb.WithFilerClient(false, 0, option.Filer, option.GrpcDialOption, func(client filer_pb.SeaweedFilerClient) error {
content, err = filer.ReadInsideFiler(client, filer.IamConfigDirectory, filer.IamIdentityFile)
return err
})
if err != nil {
return fmt.Errorf("read S3 config: %v", err)
}
return iam.LoadS3ApiConfigurationFromBytes(content)
}
func (iam *IdentityAccessManagement) loadS3ApiConfigurationFromFile(fileName string) error {
content, readErr := os.ReadFile(fileName)
if readErr != nil {
glog.Warningf("fail to read %s : %v", fileName, readErr)
return fmt.Errorf("fail to read %s : %v", fileName, readErr)
}
return iam.LoadS3ApiConfigurationFromBytes(content)
}
func (iam *IdentityAccessManagement) LoadS3ApiConfigurationFromBytes(content []byte) error {
s3ApiConfiguration := &iam_pb.S3ApiConfiguration{}
if err := filer.ParseS3ConfigurationFromBytes(content, s3ApiConfiguration); err != nil {
glog.Warningf("unmarshal error: %v", err)
return fmt.Errorf("unmarshal error: %v", err)
}
if err := filer.CheckDuplicateAccessKey(s3ApiConfiguration); err != nil {
return err
}
if err := iam.loadS3ApiConfiguration(s3ApiConfiguration); err != nil {
return err
}
return nil
}
func (iam *IdentityAccessManagement) loadS3ApiConfiguration(config *iam_pb.S3ApiConfiguration) error {
var identities []*Identity
for _, ident := range config.Identities {
t := &Identity{
Name: ident.Name,
AccountId: s3account.AccountAdmin.Id,
Credentials: nil,
Actions: nil,
}
if ident.Name == s3account.AccountAnonymous.Name {
if ident.AccountId != "" && ident.AccountId != s3account.AccountAnonymous.Id {
glog.Warningf("anonymous identity is associated with a non-anonymous account ID, the association is invalid")
}
t.AccountId = s3account.AccountAnonymous.Id
IdentityAnonymous = t
} else {
if len(ident.AccountId) > 0 {
t.AccountId = ident.AccountId
}
}
for _, action := range ident.Actions {
t.Actions = append(t.Actions, Action(action))
}
for _, cred := range ident.Credentials {
t.Credentials = append(t.Credentials, &Credential{
AccessKey: cred.AccessKey,
SecretKey: cred.SecretKey,
})
}
identities = append(identities, t)
}
if IdentityAnonymous == nil {
IdentityAnonymous = &Identity{
Name: s3account.AccountAnonymous.Name,
AccountId: s3account.AccountAnonymous.Id,
}
}
iam.m.Lock()
// atomically switch
iam.identities = identities
if !iam.isAuthEnabled { // one-directional, no toggling
iam.isAuthEnabled = len(identities) > 0
}
iam.m.Unlock()
return nil
}
func (iam *IdentityAccessManagement) isEnabled() bool {
return iam.isAuthEnabled
}
func (iam *IdentityAccessManagement) lookupByAccessKey(accessKey string) (identity *Identity, cred *Credential, found bool) {
iam.m.RLock()
defer iam.m.RUnlock()
for _, ident := range iam.identities {
for _, cred := range ident.Credentials {
// println("checking", ident.Name, cred.AccessKey)
if cred.AccessKey == accessKey {
return ident, cred, true
}
}
}
glog.V(1).Infof("could not find accessKey %s", accessKey)
return nil, nil, false
}
func (iam *IdentityAccessManagement) lookupAnonymous() (identity *Identity, found bool) {
iam.m.RLock()
defer iam.m.RUnlock()
for _, ident := range iam.identities {
if ident.isAnonymous() {
return ident, true
}
}
return nil, false
}
func (iam *IdentityAccessManagement) Auth(f http.HandlerFunc, action Action) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if !iam.isEnabled() {
f(w, r)
return
}
identity, errCode := iam.authRequest(r, action)
if errCode == s3err.ErrNone {
if identity != nil && identity.Name != "" {
r.Header.Set(s3_constants.AmzIdentityId, identity.Name)
if identity.isAdmin() {
r.Header.Set(s3_constants.AmzIsAdmin, "true")
} else if _, ok := r.Header[s3_constants.AmzIsAdmin]; ok {
r.Header.Del(s3_constants.AmzIsAdmin)
}
}
f(w, r)
return
}
s3err.WriteErrorResponse(w, r, errCode)
}
}
// check whether the request has valid access keys
func (iam *IdentityAccessManagement) authRequest(r *http.Request, action Action) (*Identity, s3err.ErrorCode) {
var identity *Identity
var s3Err s3err.ErrorCode
var found bool
var authType string
switch getRequestAuthType(r) {
case authTypeStreamingSigned:
return identity, s3err.ErrNone
case authTypeUnknown:
glog.V(3).Infof("unknown auth type")
r.Header.Set(s3_constants.AmzAuthType, "Unknown")
return identity, s3err.ErrAccessDenied
case authTypePresignedV2, authTypeSignedV2:
glog.V(3).Infof("v2 auth type")
identity, s3Err = iam.isReqAuthenticatedV2(r)
authType = "SigV2"
case authTypeSigned, authTypePresigned:
glog.V(3).Infof("v4 auth type")
identity, s3Err = iam.reqSignatureV4Verify(r)
authType = "SigV4"
case authTypePostPolicy:
glog.V(3).Infof("post policy auth type")
r.Header.Set(s3_constants.AmzAuthType, "PostPolicy")
return identity, s3err.ErrNone
case authTypeJWT:
glog.V(3).Infof("jwt auth type")
r.Header.Set(s3_constants.AmzAuthType, "Jwt")
return identity, s3err.ErrNotImplemented
case authTypeAnonymous:
authType = "Anonymous"
identity, found = iam.lookupAnonymous()
if !found {
r.Header.Set(s3_constants.AmzAuthType, authType)
return identity, s3err.ErrAccessDenied
}
default:
return identity, s3err.ErrNotImplemented
}
if len(authType) > 0 {
r.Header.Set(s3_constants.AmzAuthType, authType)
}
if s3Err != s3err.ErrNone {
return identity, s3Err
}
glog.V(3).Infof("user name: %v actions: %v, action: %v", identity.Name, identity.Actions, action)
bucket, object := s3_constants.GetBucketAndObject(r)
if !identity.canDo(action, bucket, object) {
return identity, s3err.ErrAccessDenied
}
if !identity.isAnonymous() {
r.Header.Set(s3_constants.AmzAccountId, identity.AccountId)
}
return identity, s3err.ErrNone
}
func (iam *IdentityAccessManagement) authUser(r *http.Request) (*Identity, s3err.ErrorCode) {
var identity *Identity
var s3Err s3err.ErrorCode
var found bool
var authType string
switch getRequestAuthType(r) {
case authTypeStreamingSigned:
return identity, s3err.ErrNone
case authTypeUnknown:
glog.V(3).Infof("unknown auth type")
r.Header.Set(s3_constants.AmzAuthType, "Unknown")
return identity, s3err.ErrAccessDenied
case authTypePresignedV2, authTypeSignedV2:
glog.V(3).Infof("v2 auth type")
identity, s3Err = iam.isReqAuthenticatedV2(r)
authType = "SigV2"
case authTypeSigned, authTypePresigned:
glog.V(3).Infof("v4 auth type")
identity, s3Err = iam.reqSignatureV4Verify(r)
authType = "SigV4"
case authTypePostPolicy:
glog.V(3).Infof("post policy auth type")
r.Header.Set(s3_constants.AmzAuthType, "PostPolicy")
return identity, s3err.ErrNone
case authTypeJWT:
glog.V(3).Infof("jwt auth type")
r.Header.Set(s3_constants.AmzAuthType, "Jwt")
return identity, s3err.ErrNotImplemented
case authTypeAnonymous:
authType = "Anonymous"
identity, found = iam.lookupAnonymous()
if !found {
r.Header.Set(s3_constants.AmzAuthType, authType)
return identity, s3err.ErrAccessDenied
}
default:
return identity, s3err.ErrNotImplemented
}
if len(authType) > 0 {
r.Header.Set(s3_constants.AmzAuthType, authType)
}
glog.V(3).Infof("auth error: %v", s3Err)
if s3Err != s3err.ErrNone {
return identity, s3Err
}
return identity, s3err.ErrNone
}
func (identity *Identity) canDo(action Action, bucket string, objectKey string) bool {
if identity.isAdmin() {
return true
}
for _, a := range identity.Actions {
if a == action {
return true
}
}
if bucket == "" {
return false
}
target := string(action) + ":" + bucket + objectKey
adminTarget := s3_constants.ACTION_ADMIN + ":" + bucket + objectKey
limitedByBucket := string(action) + ":" + bucket
adminLimitedByBucket := s3_constants.ACTION_ADMIN + ":" + bucket
for _, a := range identity.Actions {
act := string(a)
if strings.HasSuffix(act, "*") {
if strings.HasPrefix(target, act[:len(act)-1]) {
return true
}
if strings.HasPrefix(adminTarget, act[:len(act)-1]) {
return true
}
} else {
if act == limitedByBucket {
return true
}
if act == adminLimitedByBucket {
return true
}
}
}
return false
}
func (identity *Identity) isAdmin() bool {
for _, a := range identity.Actions {
if a == "Admin" {
return true
}
}
return false
}