Compare commits

...

8 Commits

17 changed files with 293 additions and 86 deletions
+1 -1
View File
@@ -90,7 +90,7 @@ func (api objectAPIHandlers) PutBucketACLHandler(w http.ResponseWriter, r *http.
if aclHeader == "" {
acl := &accessControlPolicy{}
if err = xmlDecoder(r.Body, acl, r.ContentLength); err != nil {
if err == io.EOF {
if terr, ok := err.(*xml.SyntaxError); ok && terr.Msg == io.EOF.Error() {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMissingSecurityHeader),
r.URL)
return
+36 -5
View File
@@ -22,6 +22,7 @@ import (
"fmt"
"runtime"
"strconv"
"time"
"github.com/minio/madmin-go/v2"
"github.com/minio/minio/internal/logger"
@@ -60,13 +61,43 @@ func activeListeners() int {
return int(globalHTTPListen.Subscribers()) + int(globalTrace.Subscribers())
}
func waitForLowHTTPReq() {
var currentIO func() int
if httpServer := newHTTPServerFn(); httpServer != nil {
currentIO = httpServer.GetRequestCount
func waitForLowIO(maxIO int, maxWait time.Duration, currentIO func() int) {
// No need to wait run at full speed.
if maxIO <= 0 {
return
}
globalHealConfig.Wait(currentIO, activeListeners)
const waitTick = 100 * time.Millisecond
tmpMaxWait := maxWait
for currentIO() >= maxIO {
if tmpMaxWait > 0 {
if tmpMaxWait < waitTick {
time.Sleep(tmpMaxWait)
} else {
time.Sleep(waitTick)
}
tmpMaxWait -= waitTick
}
if tmpMaxWait <= 0 {
return
}
}
}
func currentHTTPIO() int {
httpServer := newHTTPServerFn()
if httpServer == nil {
return 0
}
return httpServer.GetRequestCount() - activeListeners()
}
func waitForLowHTTPReq() {
maxIO, maxWait, _ := globalHealConfig.Clone()
waitForLowIO(maxIO, maxWait, currentHTTPIO)
}
func initBackgroundHealing(ctx context.Context, objAPI ObjectLayer) {
+63 -24
View File
@@ -21,6 +21,7 @@ import (
"context"
"errors"
"fmt"
"runtime"
"sync"
"time"
@@ -40,6 +41,8 @@ import (
// BucketMetadataSys captures all bucket metadata for a given cluster.
type BucketMetadataSys struct {
objAPI ObjectLayer
sync.RWMutex
metadataMap map[string]BucketMetadata
}
@@ -386,39 +389,41 @@ func (sys *BucketMetadataSys) Init(ctx context.Context, buckets []BucketInfo, ob
return errServerNotInitialized
}
sys.objAPI = objAPI
// Load bucket metadata sys in background
go sys.load(ctx, buckets, objAPI)
go sys.init(ctx, buckets)
return nil
}
func (sys *BucketMetadataSys) loadBucketMetadata(ctx context.Context, bucket BucketInfo) error {
meta, err := loadBucketMetadata(ctx, sys.objAPI, bucket.Name)
if err != nil {
return err
}
sys.Lock()
sys.metadataMap[bucket.Name] = meta
sys.Unlock()
globalEventNotifier.set(bucket, meta) // set notification targets
globalBucketTargetSys.set(bucket, meta) // set remote replication targets
return nil
}
// concurrently load bucket metadata to speed up loading bucket metadata.
func (sys *BucketMetadataSys) concurrentLoad(ctx context.Context, buckets []BucketInfo, objAPI ObjectLayer) {
func (sys *BucketMetadataSys) concurrentLoad(ctx context.Context, buckets []BucketInfo) {
g := errgroup.WithNErrs(len(buckets))
for index := range buckets {
index := index
g.Go(func() error {
_, _ = objAPI.HealBucket(ctx, buckets[index].Name, madmin.HealOpts{
_, _ = sys.objAPI.HealBucket(ctx, buckets[index].Name, madmin.HealOpts{
// Ensure heal opts for bucket metadata be deep healed all the time.
ScanMode: madmin.HealDeepScan,
Recreate: true,
})
meta, err := loadBucketMetadata(ctx, objAPI, buckets[index].Name)
if err != nil {
if !globalIsErasure && !globalIsDistErasure && errors.Is(err, errVolumeNotFound) {
meta = newBucketMetadata(buckets[index].Name)
} else {
return err
}
}
sys.Lock()
sys.metadataMap[buckets[index].Name] = meta
sys.Unlock()
globalEventNotifier.set(buckets[index], meta) // set notification targets
globalBucketTargetSys.set(buckets[index], meta) // set remote replication targets
return nil
return sys.loadBucketMetadata(ctx, buckets[index])
}, index)
}
for _, err := range g.Wait() {
@@ -428,17 +433,51 @@ func (sys *BucketMetadataSys) concurrentLoad(ctx context.Context, buckets []Buck
}
}
func (sys *BucketMetadataSys) refreshBucketsMetadataLoop(ctx context.Context) {
const bucketMetadataRefresh = 15 * time.Minute
t := time.NewTimer(bucketMetadataRefresh)
defer t.Stop()
for {
select {
case <-ctx.Done():
return
case <-t.C:
buckets, err := sys.objAPI.ListBuckets(ctx, BucketOptions{})
if err != nil {
logger.LogIf(ctx, err)
continue
}
for i := range buckets {
err := sys.loadBucketMetadata(ctx, buckets[i])
if err != nil {
logger.LogIf(ctx, err)
continue
}
// Check if there is a spare core, wait 100ms instead
waitForLowIO(runtime.NumCPU(), 100*time.Millisecond, currentHTTPIO)
}
t.Reset(bucketMetadataRefresh)
}
}
}
// Loads bucket metadata for all buckets into BucketMetadataSys.
func (sys *BucketMetadataSys) load(ctx context.Context, buckets []BucketInfo, objAPI ObjectLayer) {
func (sys *BucketMetadataSys) init(ctx context.Context, buckets []BucketInfo) {
count := 100 // load 100 bucket metadata at a time.
for {
if len(buckets) < count {
sys.concurrentLoad(ctx, buckets, objAPI)
return
sys.concurrentLoad(ctx, buckets)
break
}
sys.concurrentLoad(ctx, buckets[:count], objAPI)
sys.concurrentLoad(ctx, buckets[:count])
buckets = buckets[count:]
}
if globalIsDistErasure {
go sys.refreshBucketsMetadataLoop(ctx)
}
}
// Reset the state of the BucketMetadataSys.
+5
View File
@@ -75,6 +75,11 @@ func getConditionValues(r *http.Request, lc string, cred auth.Credentials) map[s
groups = cred.Groups
)
if cred.IsTemp() || cred.IsServiceAccount() {
// For derived credentials, check the parent user's permissions.
username = cred.ParentUser
}
principalType := "Anonymous"
if username != "" {
principalType = "User"
+5 -6
View File
@@ -411,6 +411,11 @@ func (er *erasureObjects) healObject(ctx context.Context, bucket string, object
erasure.ShardFileSize(latestMeta.Parts[0].ActualSize) < smallFileThreshold)
}
result.ObjectSize, err = latestMeta.GetActualSize()
if err != nil {
return result, err
}
// Loop to find number of disks with valid data, per-drive
// data state and a list of outdated disks on which data needs
// to be healed.
@@ -421,9 +426,6 @@ func (er *erasureObjects) healObject(ctx context.Context, bucket string, object
switch {
case v != nil:
driveState = madmin.DriveStateOk
// If data is sane on any one disk, we can
// extract the correct object size.
result.ObjectSize = partsMetadata[i].Size
case errs[i] == errDiskNotFound, dataErrs[i] == errDiskNotFound:
driveState = madmin.DriveStateOffline
case errs[i] == errFileNotFound, errs[i] == errFileVersionNotFound, errs[i] == errVolumeNotFound:
@@ -673,9 +675,6 @@ func (er *erasureObjects) healObject(ctx context.Context, bucket string, object
})
}
// Set the size of the object in the heal result
result.ObjectSize = latestMeta.Size
return result, nil
}
+49
View File
@@ -23,15 +23,18 @@ import (
"fmt"
"net/http"
"sort"
"strconv"
"strings"
"time"
"github.com/minio/minio/internal/amztime"
"github.com/minio/minio/internal/bucket/replication"
"github.com/minio/minio/internal/crypto"
"github.com/minio/minio/internal/hash/sha256"
xhttp "github.com/minio/minio/internal/http"
"github.com/minio/minio/internal/logger"
"github.com/minio/minio/internal/sync/errgroup"
"github.com/minio/sio"
)
// Object was stored with additional erasure codes due to degraded system at upload time
@@ -104,6 +107,52 @@ func (fi FileInfo) IsValid() bool {
correctIndexes)
}
func (fi FileInfo) checkMultipart() (int64, bool) {
if len(fi.Parts) == 0 {
return 0, false
}
if !crypto.IsMultiPart(fi.Metadata) {
return 0, false
}
var size int64
for _, part := range fi.Parts {
psize, err := sio.DecryptedSize(uint64(part.Size))
if err != nil {
return 0, false
}
size += int64(psize)
}
return size, len(extractETag(fi.Metadata)) != 32
}
// GetActualSize - returns the actual size of the stored object
func (fi FileInfo) GetActualSize() (int64, error) {
if _, ok := fi.Metadata[ReservedMetadataPrefix+"compression"]; ok {
sizeStr, ok := fi.Metadata[ReservedMetadataPrefix+"actual-size"]
if !ok {
return -1, errInvalidDecompressedSize
}
size, err := strconv.ParseInt(sizeStr, 10, 64)
if err != nil {
return -1, errInvalidDecompressedSize
}
return size, nil
}
if _, ok := crypto.IsEncrypted(fi.Metadata); ok {
size, ok := fi.checkMultipart()
if !ok {
size, err := sio.DecryptedSize(uint64(fi.Size))
if err != nil {
err = errObjectTampered // assign correct error type
}
return int64(size), err
}
return size, nil
}
return fi.Size, nil
}
// ToObjectInfo - Converts metadata to object info.
func (fi FileInfo) ToObjectInfo(bucket, object string, versioned bool) ObjectInfo {
object = decodeDirObject(object)
+12
View File
@@ -1207,6 +1207,7 @@ func (z *erasureServerPools) ListObjectVersions(ctx context.Context, bucket, pre
if marker == "" && versionMarker != "" {
return loi, NotImplemented{}
}
opts := listPathOptions{
Bucket: bucket,
Prefix: prefix,
@@ -1217,6 +1218,17 @@ func (z *erasureServerPools) ListObjectVersions(ctx context.Context, bucket, pre
AskDisks: globalAPIConfig.getListQuorum(),
Versioned: true,
}
// Shortcut for APN/1.0 Veeam/1.0 Backup/10.0
// It requests unique blocks with a specific prefix.
// We skip scanning the parent directory for
// more objects matching the prefix.
ri := logger.GetReqInfo(ctx)
if ri != nil && strings.Contains(ri.UserAgent, `1.0 Veeam/1.0 Backup`) && strings.HasSuffix(prefix, ".blk") {
opts.BaseDir = prefix
opts.Transient = true
}
// set bucket metadata in opts
opts.setBucketMeta(ctx)
+3 -1
View File
@@ -96,7 +96,9 @@ func (z *erasureServerPools) listPath(ctx context.Context, o *listPathOptions) (
// Decode and get the optional list id from the marker.
o.parseMarker()
o.BaseDir = baseDirFromPrefix(o.Prefix)
if o.BaseDir == "" {
o.BaseDir = baseDirFromPrefix(o.Prefix)
}
o.Transient = o.Transient || isReservedOrInvalidBucket(o.Bucket, false)
o.SetFilter()
if o.Transient {
+1
View File
@@ -49,6 +49,7 @@ type listPathOptions struct {
Bucket string
// Directory inside the bucket.
// When unset listPath will set this based on Prefix
BaseDir string
// Scan/return only content with prefix.
+1 -1
View File
@@ -361,7 +361,7 @@ func (sts *stsAPIHandlers) AssumeRoleWithSSO(w http.ResponseWriter, r *http.Requ
}
// Validate JWT; check clientID in claims matches the one associated with the roleArn
if err := globalOpenIDConfig.Validate(roleArn, token, accessToken, r.Form.Get(stsDurationSeconds), claims); err != nil {
if err := globalOpenIDConfig.Validate(r.Context(), roleArn, token, accessToken, r.Form.Get(stsDurationSeconds), claims); err != nil {
switch err {
case openid.ErrTokenExpired:
switch action {
+94
View File
@@ -38,6 +38,7 @@ func runAllIAMSTSTests(suite *TestSuiteIAM, c *check) {
suite.TestSTS(c)
suite.TestSTSWithDenyDeleteVersion(c)
suite.TestSTSWithTags(c)
suite.TestSTSServiceAccountsWithUsername(c)
suite.TestSTSWithGroupPolicy(c)
suite.TearDownSuite(c)
}
@@ -74,6 +75,99 @@ func TestIAMInternalIDPSTSServerSuite(t *testing.T) {
}
}
func (s *TestSuiteIAM) TestSTSServiceAccountsWithUsername(c *check) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
bucket := "dillon-bucket"
err := s.client.MakeBucket(ctx, bucket, minio.MakeBucketOptions{})
if err != nil {
c.Fatalf("bucket create error: %v", err)
}
// Create policy
policy := "mypolicy-username"
policyBytes := []byte(`{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:*"
],
"Resource": [
"arn:aws:s3:::${aws:username}-*"
]
}
]
}`)
err = s.adm.AddCannedPolicy(ctx, policy, policyBytes)
if err != nil {
c.Fatalf("policy add error: %v", err)
}
if err = s.adm.AddUser(ctx, "dillon", "dillon-123"); err != nil {
c.Fatalf("policy add error: %v", err)
}
err = s.adm.SetPolicy(ctx, policy, "dillon", false)
if err != nil {
c.Fatalf("Unable to set policy: %v", err)
}
assumeRole := cr.STSAssumeRole{
Client: s.TestSuiteCommon.client,
STSEndpoint: s.endPoint,
Options: cr.STSAssumeRoleOptions{
AccessKey: "dillon",
SecretKey: "dillon-123",
Location: "",
},
}
value, err := assumeRole.Retrieve()
if err != nil {
c.Fatalf("Expected to generate STS creds, got err: %#v", err)
}
// Check that the LDAP sts cred is actually working.
minioClient, err := minio.New(s.endpoint, &minio.Options{
Creds: cr.NewStaticV4(value.AccessKeyID, value.SecretAccessKey, value.SessionToken),
Secure: s.secure,
Transport: s.TestSuiteCommon.client.Transport,
})
if err != nil {
c.Fatalf("Error initializing client: %v", err)
}
// Validate that the client from sts creds can access the bucket.
c.mustListObjects(ctx, minioClient, bucket)
// Create an madmin client with user creds
userAdmClient, err := madmin.NewWithOptions(s.endpoint, &madmin.Options{
Creds: cr.NewStaticV4(value.AccessKeyID, value.SecretAccessKey, value.SessionToken),
Secure: s.secure,
})
if err != nil {
c.Fatalf("Err creating user admin client: %v", err)
}
userAdmClient.SetCustomTransport(s.TestSuiteCommon.client.Transport)
// Create svc acc
cr := c.mustCreateSvcAccount(ctx, value.AccessKeyID, userAdmClient)
svcClient := s.getUserClient(c, cr.AccessKey, cr.SecretKey, "")
// 1. Check S3 access for service account ListObjects()
c.mustListObjects(ctx, svcClient, bucket)
// 2. Check S3 access for upload
c.mustUpload(ctx, svcClient, bucket)
// 3. Check S3 access for download
c.mustDownload(ctx, svcClient, bucket)
}
func (s *TestSuiteIAM) TestSTSWithDenyDeleteVersion(c *check) {
ctx, cancel := context.WithTimeout(context.Background(), testDefaultTimeout)
defer cancel()
+1 -1
View File
@@ -326,4 +326,4 @@ minio server /data
## Explore Further
* [MinIO Quickstart Guide](https://min.io/docs/minio/linux/index.html#quickstart-for-linux)
* [Configure MinIO Server with TLS](https://min.io/docs/minio/linux/operations/network-encryption.htmls)
* [Configure MinIO Server with TLS](https://min.io/docs/minio/linux/operations/network-encryption.html)
@@ -2,7 +2,7 @@ version: '3.7'
# Settings and configurations that are common for all containers
x-minio-common: &minio-common
image: quay.io/minio/minio:RELEASE.2023-01-31T02-24-19Z
image: quay.io/minio/minio:RELEASE.2023-02-09T05-16-53Z
command: server --console-address ":9001" http://minio{1...4}/data{1...2}
expose:
- "9000"
+4 -32
View File
@@ -70,39 +70,11 @@ func (opts Config) BitrotScanCycle() (d time.Duration) {
return opts.cache.bitrotCycle
}
// Wait waits for IOCount to go down or max sleep to elapse before returning.
// usually used in healing paths to wait for specified amount of time to
// throttle healing.
func (opts Config) Wait(currentIO func() int, activeListeners func() int) {
// Clone safely the heal configuration
func (opts Config) Clone() (int, time.Duration, string) {
configMutex.RLock()
maxIO, maxWait := opts.IOCount, opts.Sleep
configMutex.RUnlock()
// No need to wait run at full speed.
if maxIO <= 0 {
return
}
// At max 10 attempts to wait with 100 millisecond interval before proceeding
waitTick := 100 * time.Millisecond
tmpMaxWait := maxWait
if currentIO != nil {
for currentIO() >= maxIO+activeListeners() {
if tmpMaxWait > 0 {
if tmpMaxWait < waitTick {
time.Sleep(tmpMaxWait)
} else {
time.Sleep(waitTick)
}
tmpMaxWait -= waitTick
}
if tmpMaxWait <= 0 {
return
}
}
}
defer configMutex.RUnlock()
return opts.IOCount, opts.Sleep, opts.Bitrot
}
// Update updates opts with nopts
+5 -4
View File
@@ -18,6 +18,7 @@
package openid
import (
"context"
"encoding/json"
"errors"
"fmt"
@@ -140,7 +141,7 @@ const (
)
// Validate - validates the id_token.
func (r *Config) Validate(arn arn.ARN, token, accessToken, dsecs string, claims jwtgo.MapClaims) error {
func (r *Config) Validate(ctx context.Context, arn arn.ARN, token, accessToken, dsecs string, claims jwtgo.MapClaims) error {
jp := new(jwtgo.Parser)
jp.ValidMethods = []string{
"RS256", "RS384", "RS512",
@@ -184,7 +185,7 @@ func (r *Config) Validate(arn arn.ARN, token, accessToken, dsecs string, claims
return err
}
if err = r.updateUserinfoClaims(arn, accessToken, claims); err != nil {
if err = r.updateUserinfoClaims(ctx, arn, accessToken, claims); err != nil {
return err
}
@@ -223,7 +224,7 @@ func (r *Config) Validate(arn arn.ARN, token, accessToken, dsecs string, claims
return nil
}
func (r *Config) updateUserinfoClaims(arn arn.ARN, accessToken string, claims map[string]interface{}) error {
func (r *Config) updateUserinfoClaims(ctx context.Context, arn arn.ARN, accessToken string, claims map[string]interface{}) error {
pCfg, ok := r.arnProviderCfgsMap[arn]
// If claim user info is enabled, get claims from userInfo
// and overwrite them with the claims from JWT.
@@ -231,7 +232,7 @@ func (r *Config) updateUserinfoClaims(arn arn.ARN, accessToken string, claims ma
if accessToken == "" {
return errors.New("access_token is mandatory if user_info claim is enabled")
}
uclaims, err := pCfg.UserInfo(accessToken, r.transport)
uclaims, err := pCfg.UserInfo(ctx, accessToken, r.transport)
if err != nil {
return err
}
+3 -2
View File
@@ -19,6 +19,7 @@ package openid
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"fmt"
@@ -148,7 +149,7 @@ func TestJWTHMACType(t *testing.T) {
}
var claims jwtgo.MapClaims
if err = cfg.Validate(DummyRoleARN, token, "", "", claims); err != nil {
if err = cfg.Validate(context.Background(), DummyRoleARN, token, "", "", claims); err != nil {
t.Fatal(err)
}
}
@@ -200,7 +201,7 @@ func TestJWT(t *testing.T) {
}
var claims jwtgo.MapClaims
if err = cfg.Validate(DummyRoleARN, u.Query().Get("Token"), "", "", claims); err == nil {
if err = cfg.Validate(context.Background(), DummyRoleARN, u.Query().Get("Token"), "", "", claims); err == nil {
t.Fatal(err)
}
}
@@ -18,6 +18,7 @@
package openid
import (
"context"
"encoding/json"
"errors"
"fmt"
@@ -108,23 +109,25 @@ func (p *providerCfg) GetRoleArn() string {
// claims as part of the normal oauth2 flow, instead rely
// on service providers making calls to IDP to fetch additional
// claims available from the UserInfo endpoint
func (p *providerCfg) UserInfo(accessToken string, transport http.RoundTripper) (map[string]interface{}, error) {
func (p *providerCfg) UserInfo(ctx context.Context, accessToken string, transport http.RoundTripper) (map[string]interface{}, error) {
if p.JWKS.URL == nil || p.JWKS.URL.String() == "" {
return nil, errors.New("openid not configured")
}
client := &http.Client{
Transport: transport,
}
req, err := http.NewRequest(http.MethodPost, p.DiscoveryDoc.UserInfoEndpoint, nil)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, p.DiscoveryDoc.UserInfoEndpoint, nil)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
if accessToken != "" {
req.Header.Set("Authorization", "Bearer "+accessToken)
}
client := &http.Client{
Transport: transport,
}
resp, err := client.Do(req)
if err != nil {
return nil, err
@@ -140,10 +143,8 @@ func (p *providerCfg) UserInfo(accessToken string, transport http.RoundTripper)
return nil, errors.New(resp.Status)
}
dec := json.NewDecoder(resp.Body)
claims := map[string]interface{}{}
if err = dec.Decode(&claims); err != nil {
if err = json.NewDecoder(resp.Body).Decode(&claims); err != nil {
// uncomment this for debugging when needed.
// reqBytes, _ := httputil.DumpRequest(req, false)
// fmt.Println(string(reqBytes))