mirror of
https://github.com/pgsty/minio.git
synced 2026-08-11 16:53:28 +03:00
Compare commits
75 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 892a204013 | |||
| 0e6aedc7ed | |||
| c547a4d835 | |||
| fc9668baa5 | |||
| ba17d46f15 | |||
| 54a4f93854 | |||
| bdd816488d | |||
| 36dcfee2f7 | |||
| 4d13ddf6b3 | |||
| 9e25475475 | |||
| e955aa7f2a | |||
| 81d2b54dfd | |||
| 7956ff0313 | |||
| 9ff25fb64b | |||
| 04df69f633 | |||
| 908eb57795 | |||
| ecfae074dc | |||
| be5d394e56 | |||
| 849a27ee61 | |||
| 062f3ea43a | |||
| 5cfedcfe33 | |||
| 4d2fc530d0 | |||
| 3970204009 | |||
| f046f557fa | |||
| 401958938d | |||
| 566cffe53d | |||
| 028bc2f9be | |||
| 813d9bc316 | |||
| 79ba458051 | |||
| cf220be9b5 | |||
| c433572585 | |||
| a42b576382 | |||
| fb9b53026d | |||
| 2ac54e5a7b | |||
| 8eecdc6d1f | |||
| 50577e2bd2 | |||
| 7bc1f986e8 | |||
| d796621ccc | |||
| 751e9fb7be | |||
| f6113264f4 | |||
| a3534a730b | |||
| 7f8b8a0e43 | |||
| bd6f7b6d83 | |||
| b0a4beb66a | |||
| 472c2d828c | |||
| 01ee49045e | |||
| 7bd9f821dd | |||
| b20ecc7b54 | |||
| 61eb9d4e29 | |||
| 43eb5a001c | |||
| f58692abb7 | |||
| c1760fb764 | |||
| e9bc0e7e98 | |||
| ffcadcd99e | |||
| 7a733a8d54 | |||
| ce97313fda | |||
| 7b81967a3c | |||
| ff811f594b | |||
| 0bf80b3c89 | |||
| ae3b369fe1 | |||
| 77b15e7194 | |||
| 20537f974e | |||
| 4476a64bdf | |||
| d4b701576e | |||
| 721c053712 | |||
| e3071157f0 | |||
| c07af89e48 | |||
| 9c846106fa | |||
| cf94d1f1f1 | |||
| 6187440f35 | |||
| 57b7c3494f | |||
| dda18c28c5 | |||
| f8d6eaaa96 | |||
| 47d4fabb58 | |||
| 80039f60d5 |
@@ -11,6 +11,9 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Tests with Go ${{ matrix.go-version }} on ${{ matrix.os }}
|
||||
@@ -20,10 +23,11 @@ jobs:
|
||||
go-version: [1.17.x]
|
||||
os: [ubuntu-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-go@v2
|
||||
- uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2
|
||||
- uses: actions/setup-go@bfdd3570ce990073878bf10f6b2d79082de49492 # v2
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
check-latest: true
|
||||
- name: Build on ${{ matrix.os }}
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
env:
|
||||
|
||||
@@ -21,9 +21,10 @@ jobs:
|
||||
os: [ubuntu-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-go@v2
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
check-latest: true
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
|
||||
@@ -21,9 +21,10 @@ jobs:
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-go@v2
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
check-latest: true
|
||||
- uses: actions/cache@v2
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
with:
|
||||
|
||||
@@ -21,9 +21,10 @@ jobs:
|
||||
os: [ubuntu-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-go@v2
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
check-latest: true
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
|
||||
@@ -65,9 +65,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-go@v2
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
check-latest: true
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
|
||||
@@ -22,9 +22,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-go@v2
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
check-latest: true
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
|
||||
@@ -22,10 +22,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/setup-go@v2
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
|
||||
check-latest: true
|
||||
- name: Start upgrade tests
|
||||
run: |
|
||||
make test-upgrade
|
||||
|
||||
+34
-19
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2015-2021 MinIO, Inc.
|
||||
// Copyright (c) 2015-2022 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
@@ -18,10 +18,10 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
crand "crypto/rand"
|
||||
"crypto/subtle"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -1685,7 +1685,10 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
|
||||
logger.LogIf(ctx, enc.Encode(healthInfo))
|
||||
}
|
||||
|
||||
deadline := 1 * time.Hour
|
||||
deadline := 10 * time.Second // Default deadline is 10secs for health diagnostics.
|
||||
if query.Get("perfnet") != "" || query.Get("perfdrive") != "" {
|
||||
deadline = 1 * time.Hour
|
||||
}
|
||||
if dstr := r.Form.Get("deadline"); dstr != "" {
|
||||
var err error
|
||||
deadline, err = time.ParseDuration(dstr)
|
||||
@@ -2332,19 +2335,9 @@ func checkConnection(endpointStr string, timeout time.Duration) error {
|
||||
ctx, cancel := context.WithTimeout(GlobalContext, timeout)
|
||||
defer cancel()
|
||||
|
||||
client := &http.Client{Transport: &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: xhttp.NewCustomDialContext(timeout),
|
||||
ResponseHeaderTimeout: 5 * time.Second,
|
||||
TLSHandshakeTimeout: 5 * time.Second,
|
||||
ExpectContinueTimeout: 5 * time.Second,
|
||||
TLSClientConfig: &tls.Config{RootCAs: globalRootCAs},
|
||||
// Go net/http automatically unzip if content-type is
|
||||
// gzip disable this feature, as we are always interested
|
||||
// in raw stream.
|
||||
DisableCompression: true,
|
||||
}}
|
||||
defer client.CloseIdleConnections()
|
||||
client := &http.Client{
|
||||
Transport: globalProxyTransport,
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodHead, endpointStr, nil)
|
||||
if err != nil {
|
||||
@@ -2437,8 +2430,7 @@ func (a adminAPIHandlers) InspectDataHandler(w http.ResponseWriter, r *http.Requ
|
||||
// of profiling data of all nodes
|
||||
zipWriter := zip.NewWriter(encw)
|
||||
defer zipWriter.Close()
|
||||
|
||||
err = o.GetRawData(ctx, volume, file, func(r io.Reader, host, disk, filename string, si StatInfo) error {
|
||||
rawDataFn := func(r io.Reader, host, disk, filename string, si StatInfo) error {
|
||||
// Prefix host+disk
|
||||
filename = path.Join(host, disk, filename)
|
||||
if si.Dir {
|
||||
@@ -2471,10 +2463,33 @@ func (a adminAPIHandlers) InspectDataHandler(w http.ResponseWriter, r *http.Requ
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
err = o.GetRawData(ctx, volume, file, rawDataFn)
|
||||
if !errors.Is(err, errFileNotFound) {
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
|
||||
// save the format.json as part of inspect by default
|
||||
if volume != minioMetaBucket && file != formatConfigFile {
|
||||
err = o.GetRawData(ctx, minioMetaBucket, formatConfigFile, rawDataFn)
|
||||
}
|
||||
if !errors.Is(err, errFileNotFound) {
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
// save args passed to inspect command
|
||||
inspectArgs := []string{fmt.Sprintf(" Inspect path: %s%s%s\n", volume, slashSeparator, file)}
|
||||
cmdLine := []string{"Server command line args: "}
|
||||
for _, pool := range globalEndpoints {
|
||||
cmdLine = append(cmdLine, pool.CmdLine)
|
||||
}
|
||||
cmdLine = append(cmdLine, "\n")
|
||||
inspectArgs = append(inspectArgs, cmdLine...)
|
||||
inspectArgsBytes := []byte(strings.Join(inspectArgs, " "))
|
||||
if err = rawDataFn(bytes.NewReader(inspectArgsBytes), "", "", "inspect-input.txt", StatInfo{
|
||||
Size: int64(len(inspectArgsBytes)),
|
||||
}); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
func createHostAnonymizerForFSMode() map[string]string {
|
||||
|
||||
@@ -50,7 +50,7 @@ func getLocalServerProperty(endpointServerPools EndpointServerPools, r *http.Req
|
||||
}
|
||||
_, present := network[nodeName]
|
||||
if !present {
|
||||
if err := isServerResolvable(endpoint, 2*time.Second); err == nil {
|
||||
if err := isServerResolvable(endpoint, 5*time.Second); err == nil {
|
||||
network[nodeName] = string(madmin.ItemOnline)
|
||||
} else {
|
||||
network[nodeName] = string(madmin.ItemOffline)
|
||||
|
||||
+44
-17
@@ -197,13 +197,7 @@ func mustGetClaimsFromToken(r *http.Request) map[string]interface{} {
|
||||
return claims
|
||||
}
|
||||
|
||||
// Fetch claims in the security token returned by the client.
|
||||
func getClaimsFromToken(token string) (map[string]interface{}, error) {
|
||||
if token == "" {
|
||||
claims := xjwt.NewMapClaims()
|
||||
return claims.Map(), nil
|
||||
}
|
||||
|
||||
func getClaimsFromTokenWithSecret(token, secret string) (map[string]interface{}, error) {
|
||||
// JWT token for x-amz-security-token is signed with admin
|
||||
// secret key, temporary credentials become invalid if
|
||||
// server admin credentials change. This is done to ensure
|
||||
@@ -212,9 +206,15 @@ func getClaimsFromToken(token string) (map[string]interface{}, error) {
|
||||
// hijacking the policies. We need to make sure that this is
|
||||
// based an admin credential such that token cannot be decoded
|
||||
// on the client side and is treated like an opaque value.
|
||||
claims, err := auth.ExtractClaims(token, globalActiveCred.SecretKey)
|
||||
claims, err := auth.ExtractClaims(token, secret)
|
||||
if err != nil {
|
||||
return nil, errAuthentication
|
||||
if subtle.ConstantTimeCompare([]byte(secret), []byte(globalActiveCred.SecretKey)) == 1 {
|
||||
return nil, errAuthentication
|
||||
}
|
||||
claims, err = auth.ExtractClaims(token, globalActiveCred.SecretKey)
|
||||
if err != nil {
|
||||
return nil, errAuthentication
|
||||
}
|
||||
}
|
||||
|
||||
// If OPA is set, return without any further checks.
|
||||
@@ -241,23 +241,50 @@ func getClaimsFromToken(token string) (map[string]interface{}, error) {
|
||||
return claims.Map(), nil
|
||||
}
|
||||
|
||||
// Fetch claims in the security token returned by the client.
|
||||
func getClaimsFromToken(token string) (map[string]interface{}, error) {
|
||||
return getClaimsFromTokenWithSecret(token, globalActiveCred.SecretKey)
|
||||
}
|
||||
|
||||
// Fetch claims in the security token returned by the client and validate the token.
|
||||
func checkClaimsFromToken(r *http.Request, cred auth.Credentials) (map[string]interface{}, APIErrorCode) {
|
||||
token := getSessionToken(r)
|
||||
if token != "" && cred.AccessKey == "" {
|
||||
// x-amz-security-token is not allowed for anonymous access.
|
||||
return nil, ErrNoAccessKey
|
||||
}
|
||||
if cred.IsServiceAccount() && token == "" {
|
||||
token = cred.SessionToken
|
||||
}
|
||||
if subtle.ConstantTimeCompare([]byte(token), []byte(cred.SessionToken)) != 1 {
|
||||
|
||||
if token == "" && cred.IsTemp() {
|
||||
// Temporary credentials should always have x-amz-security-token
|
||||
return nil, ErrInvalidToken
|
||||
}
|
||||
claims, err := getClaimsFromToken(token)
|
||||
if err != nil {
|
||||
return nil, toAPIErrorCode(r.Context(), err)
|
||||
|
||||
if token != "" && !cred.IsTemp() {
|
||||
// x-amz-security-token should not present for static credentials.
|
||||
return nil, ErrInvalidToken
|
||||
}
|
||||
return claims, ErrNone
|
||||
|
||||
if cred.IsTemp() && subtle.ConstantTimeCompare([]byte(token), []byte(cred.SessionToken)) != 1 {
|
||||
// validate token for temporary credentials only.
|
||||
return nil, ErrInvalidToken
|
||||
}
|
||||
|
||||
secret := globalActiveCred.SecretKey
|
||||
if cred.IsServiceAccount() {
|
||||
token = cred.SessionToken
|
||||
secret = cred.SecretKey
|
||||
}
|
||||
|
||||
if token != "" {
|
||||
claims, err := getClaimsFromTokenWithSecret(token, secret)
|
||||
if err != nil {
|
||||
return nil, toAPIErrorCode(r.Context(), err)
|
||||
}
|
||||
return claims, ErrNone
|
||||
}
|
||||
|
||||
claims := xjwt.NewMapClaims()
|
||||
return claims.Map(), ErrNone
|
||||
}
|
||||
|
||||
// Check request auth type verifies the incoming http request
|
||||
|
||||
@@ -24,28 +24,12 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/minio/minio/internal/kms"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
|
||||
"github.com/minio/minio/internal/sync/errgroup"
|
||||
"github.com/minio/pkg/bucket/policy"
|
||||
)
|
||||
|
||||
func concurrentDecryptETag(ctx context.Context, objects []ObjectInfo) {
|
||||
g := errgroup.WithNErrs(len(objects)).WithConcurrency(500)
|
||||
for index := range objects {
|
||||
index := index
|
||||
g.Go(func() error {
|
||||
size, err := objects[index].GetActualSize()
|
||||
if err == nil {
|
||||
objects[index].Size = size
|
||||
}
|
||||
objects[index].ETag = objects[index].GetActualETag(nil)
|
||||
return nil
|
||||
}, index)
|
||||
}
|
||||
g.Wait()
|
||||
}
|
||||
|
||||
// Validate all the ListObjects query arguments, returns an APIErrorCode
|
||||
// if one of the args do not meet the required conditions.
|
||||
// Special conditions required by MinIO server are as below
|
||||
@@ -116,7 +100,10 @@ func (api objectAPIHandlers) ListObjectVersionsHandler(w http.ResponseWriter, r
|
||||
return
|
||||
}
|
||||
|
||||
concurrentDecryptETag(ctx, listObjectVersionsInfo.Objects)
|
||||
if err = DecryptETags(ctx, GlobalKMS, listObjectVersionsInfo.Objects, kms.BatchSize()); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
response := generateListVersionsResponse(bucket, prefix, marker, versionIDMarker, delimiter, encodingType, maxkeys, listObjectVersionsInfo)
|
||||
|
||||
@@ -178,7 +165,10 @@ func (api objectAPIHandlers) ListObjectsV2MHandler(w http.ResponseWriter, r *htt
|
||||
return
|
||||
}
|
||||
|
||||
concurrentDecryptETag(ctx, listObjectsV2Info.Objects)
|
||||
if err = DecryptETags(ctx, GlobalKMS, listObjectsV2Info.Objects, kms.BatchSize()); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// The next continuation token has id@node_index format to optimize paginated listing
|
||||
nextContinuationToken := listObjectsV2Info.NextContinuationToken
|
||||
@@ -253,7 +243,10 @@ func (api objectAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http
|
||||
return
|
||||
}
|
||||
|
||||
concurrentDecryptETag(ctx, listObjectsV2Info.Objects)
|
||||
if err = DecryptETags(ctx, GlobalKMS, listObjectsV2Info.Objects, kms.BatchSize()); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
response := generateListObjectsV2Response(bucket, prefix, token, listObjectsV2Info.NextContinuationToken, startAfter,
|
||||
delimiter, encodingType, fetchOwner, listObjectsV2Info.IsTruncated,
|
||||
@@ -350,7 +343,10 @@ func (api objectAPIHandlers) ListObjectsV1Handler(w http.ResponseWriter, r *http
|
||||
return
|
||||
}
|
||||
|
||||
concurrentDecryptETag(ctx, listObjectsInfo.Objects)
|
||||
if err = DecryptETags(ctx, GlobalKMS, listObjectsInfo.Objects, kms.BatchSize()); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
response := generateListObjectsV1Response(bucket, prefix, marker, delimiter, encodingType, maxKeys, listObjectsInfo)
|
||||
|
||||
|
||||
@@ -425,7 +425,7 @@ func replicateDelete(ctx context.Context, dobj DeletedObjectReplicationInfo, obj
|
||||
wg.Add(1)
|
||||
go func(index int, tgt *TargetClient) {
|
||||
defer wg.Done()
|
||||
rinfo := replicateDeleteToTarget(ctx, dobj, objectAPI, tgt)
|
||||
rinfo := replicateDeleteToTarget(ctx, dobj, tgt)
|
||||
rinfos.Targets[index] = rinfo
|
||||
}(idx, tgt)
|
||||
}
|
||||
@@ -482,7 +482,7 @@ func replicateDelete(ctx context.Context, dobj DeletedObjectReplicationInfo, obj
|
||||
}
|
||||
}
|
||||
|
||||
func replicateDeleteToTarget(ctx context.Context, dobj DeletedObjectReplicationInfo, objectAPI ObjectLayer, tgt *TargetClient) (rinfo replicatedTargetInfo) {
|
||||
func replicateDeleteToTarget(ctx context.Context, dobj DeletedObjectReplicationInfo, tgt *TargetClient) (rinfo replicatedTargetInfo) {
|
||||
versionID := dobj.DeleteMarkerVersionID
|
||||
if versionID == "" {
|
||||
versionID = dobj.VersionID
|
||||
|
||||
+29
-4
@@ -24,6 +24,7 @@ import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/gob"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
@@ -100,12 +101,16 @@ func init() {
|
||||
PersistOnFailure: false,
|
||||
}
|
||||
|
||||
globalIsCICD = env.Get("MINIO_CI_CD", "") != "" || env.Get("CI", "") != ""
|
||||
|
||||
containers := IsKubernetes() || IsDocker() || IsBOSH() || IsDCOS() || IsPCFTile()
|
||||
|
||||
// Call to refresh will refresh names in cache. If you pass true, it will also
|
||||
// remove cached names not looked up since the last call to Refresh. It is a good idea
|
||||
// to call this method on a regular interval.
|
||||
go func() {
|
||||
var t *time.Ticker
|
||||
if IsKubernetes() || IsDocker() || IsBOSH() || IsDCOS() || IsPCFTile() {
|
||||
if containers {
|
||||
t = time.NewTicker(1 * time.Minute)
|
||||
} else {
|
||||
t = time.NewTicker(10 * time.Minute)
|
||||
@@ -664,8 +669,6 @@ func handleCommonEnvVars() {
|
||||
globalRootDiskThreshold = size
|
||||
}
|
||||
|
||||
globalIsCICD = env.Get("MINIO_CI_CD", "") != "" || env.Get("CI", "") != ""
|
||||
|
||||
domains := env.Get(config.EnvDomain, "")
|
||||
if len(domains) != 0 {
|
||||
for _, domainName := range strings.Split(domains, config.ValueSeparator) {
|
||||
@@ -796,7 +799,29 @@ func handleCommonEnvVars() {
|
||||
endpoints = append(endpoints, strings.Join(lbls, ""))
|
||||
}
|
||||
}
|
||||
certificate, err := tls.LoadX509KeyPair(env.Get(config.EnvKESClientCert, ""), env.Get(config.EnvKESClientKey, ""))
|
||||
// Manually load the certificate and private key into memory.
|
||||
// We need to check whether the private key is encrypted, and
|
||||
// if so, decrypt it using the user-provided password.
|
||||
certBytes, err := os.ReadFile(env.Get(config.EnvKESClientCert, ""))
|
||||
if err != nil {
|
||||
logger.Fatal(err, "Unable to load KES client certificate as specified by the shell environment")
|
||||
}
|
||||
keyBytes, err := os.ReadFile(env.Get(config.EnvKESClientKey, ""))
|
||||
if err != nil {
|
||||
logger.Fatal(err, "Unable to load KES client private key as specified by the shell environment")
|
||||
}
|
||||
privateKeyPEM, rest := pem.Decode(bytes.TrimSpace(keyBytes))
|
||||
if len(rest) != 0 {
|
||||
logger.Fatal(errors.New("private key contains additional data"), "Unable to load KES client private key as specified by the shell environment")
|
||||
}
|
||||
if x509.IsEncryptedPEMBlock(privateKeyPEM) {
|
||||
keyBytes, err = x509.DecryptPEMBlock(privateKeyPEM, []byte(env.Get(config.EnvKESClientPassword, "")))
|
||||
if err != nil {
|
||||
logger.Fatal(err, "Unable to decrypt KES client private key as specified by the shell environment")
|
||||
}
|
||||
keyBytes = pem.EncodeToMemory(&pem.Block{Type: privateKeyPEM.Type, Bytes: keyBytes})
|
||||
}
|
||||
certificate, err := tls.X509KeyPair(certBytes, keyBytes)
|
||||
if err != nil {
|
||||
logger.Fatal(err, "Unable to load KES client certificate as specified by the shell environment")
|
||||
}
|
||||
|
||||
+119
-5
@@ -19,6 +19,7 @@ package cmd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
@@ -35,6 +36,7 @@ import (
|
||||
|
||||
"github.com/minio/kes"
|
||||
"github.com/minio/minio/internal/crypto"
|
||||
"github.com/minio/minio/internal/etag"
|
||||
"github.com/minio/minio/internal/fips"
|
||||
xhttp "github.com/minio/minio/internal/http"
|
||||
"github.com/minio/minio/internal/kms"
|
||||
@@ -71,18 +73,130 @@ const (
|
||||
)
|
||||
|
||||
// KMSKeyID returns in AWS compatible KMS KeyID() format.
|
||||
func (o ObjectInfo) KMSKeyID() string {
|
||||
if len(o.UserDefined) == 0 {
|
||||
func (o *ObjectInfo) KMSKeyID() string { return kmsKeyIDFromMetadata(o.UserDefined) }
|
||||
|
||||
// KMSKeyID returns in AWS compatible KMS KeyID() format.
|
||||
func (o *MultipartInfo) KMSKeyID() string { return kmsKeyIDFromMetadata(o.UserDefined) }
|
||||
|
||||
// kmsKeyIDFromMetadata returns any AWS S3 KMS key ID in the
|
||||
// metadata, if any. It returns an empty ID if no key ID is
|
||||
// present.
|
||||
func kmsKeyIDFromMetadata(metadata map[string]string) string {
|
||||
const ARNPrefix = "arn:aws:kms:"
|
||||
if len(metadata) == 0 {
|
||||
return ""
|
||||
}
|
||||
kmsID, ok := o.UserDefined[crypto.MetaKeyID]
|
||||
kmsID, ok := metadata[crypto.MetaKeyID]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
if strings.HasPrefix(kmsID, "arn:aws:kms:") {
|
||||
if strings.HasPrefix(kmsID, ARNPrefix) {
|
||||
return kmsID
|
||||
}
|
||||
return "arn:aws:kms:" + kmsID
|
||||
return ARNPrefix + kmsID
|
||||
}
|
||||
|
||||
// DecryptETags dectypts all ObjectInfo ETags, if encrypted, using the KMS.
|
||||
func DecryptETags(ctx context.Context, KMS kms.KMS, objects []ObjectInfo, batchSize int) error {
|
||||
var (
|
||||
metadata = make([]map[string]string, 0, batchSize)
|
||||
buckets = make([]string, 0, batchSize)
|
||||
names = make([]string, 0, batchSize)
|
||||
)
|
||||
for len(objects) > 0 {
|
||||
var N int
|
||||
if len(objects) < batchSize {
|
||||
N = len(objects)
|
||||
} else {
|
||||
N = batchSize
|
||||
}
|
||||
|
||||
// We have to conntect the KMS only if there is at least
|
||||
// one SSE-S3 single-part object. SSE-C and SSE-KMS objects
|
||||
// don't return the plaintext MD5 ETag and the ETag of
|
||||
// SSE-S3 multipart objects is not encrypted.
|
||||
// Therefore, we can skip the expensive KMS calls whenever
|
||||
// there is no single-part SSE-S3 object entirely.
|
||||
var containsSSES3SinglePart bool
|
||||
for _, object := range objects[:N] {
|
||||
if kind, ok := crypto.IsEncrypted(object.UserDefined); ok && kind == crypto.S3 && !crypto.IsMultiPart(object.UserDefined) {
|
||||
containsSSES3SinglePart = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !containsSSES3SinglePart {
|
||||
for i := range objects[:N] {
|
||||
size, err := objects[i].GetActualSize()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
objects[i].Size = size
|
||||
objects[i].ETag = objects[i].GetActualETag(nil)
|
||||
}
|
||||
objects = objects[N:]
|
||||
continue
|
||||
}
|
||||
|
||||
// Now, there are some SSE-S3 single-part objects.
|
||||
// We only request the decryption keys for them.
|
||||
// We don't want to get the decryption keys for multipart
|
||||
// or non-SSE-S3 objects.
|
||||
//
|
||||
// Therefore, we keep a map of indicies to remember which
|
||||
// object was an SSE-S3 single-part object.
|
||||
// Then we request the decryption keys for these objects.
|
||||
// Finally, we decrypt the ETags of these objects using
|
||||
// the decryption keys.
|
||||
// However, we must also adjust the size and ETags of all
|
||||
// objects (not just the SSE-S3 single part objects).
|
||||
// For example, the ETag of SSE-KMS objects are random values
|
||||
// and the size of an SSE-KMS object must be adjusted as well.
|
||||
SSES3Objects := make(map[int]bool, 10)
|
||||
metadata = metadata[:0:N]
|
||||
buckets = buckets[:0:N]
|
||||
names = names[:0:N]
|
||||
for i, object := range objects[:N] {
|
||||
if kind, ok := crypto.IsEncrypted(object.UserDefined); ok && kind == crypto.S3 && !crypto.IsMultiPart(object.UserDefined) {
|
||||
metadata = append(metadata, object.UserDefined)
|
||||
buckets = append(buckets, object.Bucket)
|
||||
names = append(names, object.Name)
|
||||
|
||||
SSES3Objects[i] = true
|
||||
}
|
||||
}
|
||||
keys, err := crypto.S3.UnsealObjectKeys(KMS, metadata, buckets, names)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var keyIndex int
|
||||
for i := range objects[:N] {
|
||||
size, err := objects[i].GetActualSize()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
objects[i].Size = size
|
||||
|
||||
if !SSES3Objects[i] {
|
||||
objects[i].ETag = objects[i].GetActualETag(nil)
|
||||
} else {
|
||||
ETag, err := etag.Parse(objects[i].ETag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ETag.IsEncrypted() {
|
||||
tag, err := keys[keyIndex].UnsealETag(ETag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ETag = etag.ETag(tag)
|
||||
objects[i].ETag = ETag.String()
|
||||
}
|
||||
keyIndex++
|
||||
}
|
||||
}
|
||||
objects = objects[N:]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// isMultipart returns true if the current object is
|
||||
|
||||
@@ -39,8 +39,10 @@ func (er erasureObjects) MakeBucketWithLocation(ctx context.Context, bucket stri
|
||||
defer NSUpdated(bucket, slashSeparator)
|
||||
|
||||
// Verify if bucket is valid.
|
||||
if err := s3utils.CheckValidBucketNameStrict(bucket); err != nil {
|
||||
return BucketNameInvalid{Bucket: bucket}
|
||||
if !isMinioMetaBucketName(bucket) {
|
||||
if err := s3utils.CheckValidBucketNameStrict(bucket); err != nil {
|
||||
return BucketNameInvalid{Bucket: bucket}
|
||||
}
|
||||
}
|
||||
|
||||
storageDisks := er.getDisks()
|
||||
|
||||
@@ -493,11 +493,6 @@ func (er erasureObjects) getObjectInfo(ctx context.Context, bucket, object strin
|
||||
return objInfo, toObjectErr(err, bucket, object)
|
||||
}
|
||||
objInfo = fi.ToObjectInfo(bucket, object)
|
||||
if opts.VersionID != "" && !fi.VersionPurgeStatus().Empty() {
|
||||
// Make sure to return object info to provide extra information.
|
||||
return objInfo, toObjectErr(errMethodNotAllowed, bucket, object)
|
||||
}
|
||||
|
||||
if fi.Deleted {
|
||||
if opts.VersionID == "" || opts.DeleteMarker {
|
||||
return objInfo, toObjectErr(errFileNotFound, bucket, object)
|
||||
|
||||
@@ -472,13 +472,16 @@ func (z *erasureServerPools) Init(ctx context.Context) error {
|
||||
// '-1' as argument to decommission multiple pools at a time
|
||||
// but this is not a priority at the moment.
|
||||
for _, pool := range meta.returnResumablePools(1) {
|
||||
err := z.Decommission(ctx, pool.ID)
|
||||
switch err {
|
||||
case errDecommissionAlreadyRunning:
|
||||
fallthrough
|
||||
case nil:
|
||||
go z.doDecommissionInRoutine(ctx, pool.ID)
|
||||
}
|
||||
go func(pool PoolStatus) {
|
||||
switch err := z.Decommission(ctx, pool.ID); err {
|
||||
case errDecommissionAlreadyRunning:
|
||||
fallthrough
|
||||
case nil:
|
||||
z.doDecommissionInRoutine(ctx, pool.ID)
|
||||
default:
|
||||
logger.LogIf(ctx, fmt.Errorf("Unable to resume decommission of pool %v: %w", pool, err))
|
||||
}
|
||||
}(pool)
|
||||
}
|
||||
z.poolMeta = meta
|
||||
|
||||
@@ -801,12 +804,7 @@ func (z *erasureServerPools) getDecommissionPoolSpaceInfo(idx int) (pi poolSpace
|
||||
if idx+1 > len(z.serverPools) {
|
||||
return pi, errInvalidArgument
|
||||
}
|
||||
info, errs := z.serverPools[idx].StorageInfo(context.Background())
|
||||
for _, err := range errs {
|
||||
if err != nil {
|
||||
return pi, errInvalidArgument
|
||||
}
|
||||
}
|
||||
info, _ := z.serverPools[idx].StorageInfo(context.Background())
|
||||
info.Backend = z.BackendInfo()
|
||||
for _, disk := range info.Disks {
|
||||
if disk.Healing {
|
||||
@@ -962,8 +960,11 @@ func (z *erasureServerPools) StartDecommission(ctx context.Context, idx int) (er
|
||||
pathJoin(minioMetaBucket, minioConfigPrefix),
|
||||
pathJoin(minioMetaBucket, bucketMetaPrefix),
|
||||
} {
|
||||
var bucketExists BucketExists
|
||||
if err = z.MakeBucketWithLocation(ctx, metaBucket, BucketOptions{}); err != nil {
|
||||
return err
|
||||
if !errors.As(err, &bucketExists) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ import (
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/minio-go/v7/pkg/set"
|
||||
"github.com/minio/minio-go/v7/pkg/tags"
|
||||
"github.com/minio/minio/internal/bucket/lifecycle"
|
||||
"github.com/minio/minio/internal/config/storageclass"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/minio/internal/sync/errgroup"
|
||||
@@ -128,7 +129,9 @@ func newErasureServerPools(ctx context.Context, endpointServerPools EndpointServ
|
||||
if !configRetriableErrors(err) {
|
||||
logger.Fatal(err, "Unable to initialize backend")
|
||||
}
|
||||
time.Sleep(time.Duration(r.Float64() * float64(5*time.Second)))
|
||||
retry := time.Duration(r.Float64() * float64(5*time.Second))
|
||||
logger.LogIf(ctx, fmt.Errorf("Unable to initialize backend: %w, retrying in %s", err, retry))
|
||||
time.Sleep(retry)
|
||||
continue
|
||||
}
|
||||
break
|
||||
@@ -817,6 +820,11 @@ func (z *erasureServerPools) getLatestObjectInfoWithIdx(ctx context.Context, buc
|
||||
// should be returned upwards.
|
||||
return res.oi, res.zIdx, err
|
||||
}
|
||||
// When its a delete marker and versionID is empty
|
||||
// we should simply return the error right away.
|
||||
if res.oi.DeleteMarker && opts.VersionID == "" {
|
||||
return res.oi, res.zIdx, err
|
||||
}
|
||||
}
|
||||
|
||||
object = decodeDirObject(object)
|
||||
@@ -1150,6 +1158,14 @@ func maxKeysPlusOne(maxKeys int, addOne bool) int {
|
||||
func (z *erasureServerPools) ListObjects(ctx context.Context, bucket, prefix, marker, delimiter string, maxKeys int) (ListObjectsInfo, error) {
|
||||
var loi ListObjectsInfo
|
||||
|
||||
// Automatically remove the object/version is an expiry lifecycle rule can be applied
|
||||
lc, _ := globalLifecycleSys.Get(bucket)
|
||||
if lc != nil {
|
||||
if !lc.HasActiveRules(prefix, true) {
|
||||
lc = nil
|
||||
}
|
||||
}
|
||||
|
||||
if len(prefix) > 0 && maxKeys == 1 && delimiter == "" && marker == "" {
|
||||
// Optimization for certain applications like
|
||||
// - Cohesity
|
||||
@@ -1160,6 +1176,13 @@ func (z *erasureServerPools) ListObjects(ctx context.Context, bucket, prefix, ma
|
||||
// to avoid the need for ListObjects().
|
||||
objInfo, err := z.GetObjectInfo(ctx, bucket, prefix, ObjectOptions{NoLock: true})
|
||||
if err == nil {
|
||||
if lc != nil {
|
||||
action := evalActionFromLifecycle(ctx, *lc, objInfo, false)
|
||||
switch action {
|
||||
case lifecycle.DeleteVersionAction, lifecycle.DeleteAction:
|
||||
return loi, nil
|
||||
}
|
||||
}
|
||||
loi.Objects = append(loi.Objects, objInfo)
|
||||
return loi, nil
|
||||
}
|
||||
@@ -1174,6 +1197,7 @@ func (z *erasureServerPools) ListObjects(ctx context.Context, bucket, prefix, ma
|
||||
InclDeleted: false,
|
||||
AskDisks: globalAPIConfig.getListQuorum(),
|
||||
}
|
||||
|
||||
merged, err := z.listPath(ctx, &opts)
|
||||
if err != nil && err != io.EOF {
|
||||
if !isErrBucketNotFound(err) {
|
||||
|
||||
+2
-2
@@ -102,7 +102,7 @@ const (
|
||||
GlobalStaleUploadsCleanupInterval = time.Hour * 6 // 6 hrs.
|
||||
|
||||
// Refresh interval to update in-memory iam config cache.
|
||||
globalRefreshIAMInterval = 5 * time.Minute
|
||||
globalRefreshIAMInterval = 10 * time.Minute
|
||||
|
||||
// Limit of location constraint XML for unauthenticated PUT bucket operations.
|
||||
maxLocationConstraintSize = 3 * humanize.MiByte
|
||||
@@ -190,7 +190,7 @@ var (
|
||||
globalBucketTargetSys *BucketTargetSys
|
||||
// globalAPIConfig controls S3 API requests throttling,
|
||||
// healthcheck readiness deadlines and cors settings.
|
||||
globalAPIConfig = apiConfig{listQuorum: 3}
|
||||
globalAPIConfig = apiConfig{listQuorum: "strict"}
|
||||
|
||||
globalStorageClass storageclass.Config
|
||||
globalLDAPConfig xldap.Config
|
||||
|
||||
+4
-4
@@ -38,7 +38,7 @@ type apiConfig struct {
|
||||
requestsDeadline time.Duration
|
||||
requestsPool chan struct{}
|
||||
clusterDeadline time.Duration
|
||||
listQuorum int
|
||||
listQuorum string
|
||||
corsAllowOrigins []string
|
||||
// total drives per erasure set across pools.
|
||||
totalDriveCount int
|
||||
@@ -127,7 +127,7 @@ func (t *apiConfig) init(cfg api.Config, setDriveCounts []int) {
|
||||
}
|
||||
}
|
||||
|
||||
if cap(t.requestsPool) < apiRequestsMaxPerNode {
|
||||
if cap(t.requestsPool) != apiRequestsMaxPerNode {
|
||||
// Only replace if needed.
|
||||
// Existing requests will use the previous limit,
|
||||
// but new requests will use the new limit.
|
||||
@@ -136,7 +136,7 @@ func (t *apiConfig) init(cfg api.Config, setDriveCounts []int) {
|
||||
t.requestsPool = make(chan struct{}, apiRequestsMaxPerNode)
|
||||
}
|
||||
t.requestsDeadline = cfg.RequestsDeadline
|
||||
t.listQuorum = cfg.GetListQuorum()
|
||||
t.listQuorum = cfg.ListQuorum
|
||||
if globalReplicationPool != nil &&
|
||||
cfg.ReplicationWorkers != t.replicationWorkers {
|
||||
globalReplicationPool.ResizeFailedWorkers(cfg.ReplicationFailedWorkers)
|
||||
@@ -170,7 +170,7 @@ func (t *apiConfig) shouldGzipObjects() bool {
|
||||
return t.gzipObjects
|
||||
}
|
||||
|
||||
func (t *apiConfig) getListQuorum() int {
|
||||
func (t *apiConfig) getListQuorum() string {
|
||||
t.mu.RLock()
|
||||
defer t.mu.RUnlock()
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -405,6 +406,136 @@ func (iamOS *IAMObjectStore) loadMappedPolicies(ctx context.Context, userType IA
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
usersListKey = "/users/"
|
||||
svcAccListKey = "/service-accounts/"
|
||||
groupsListKey = "/groups/"
|
||||
policiesListKey = "/policies/"
|
||||
stsListKey = "/sts/"
|
||||
policyDBUsersListKey = "/policydb/users/"
|
||||
policyDBSTSUsersListKey = "/policydb/sts-users/"
|
||||
policyDBServiceAccountsListKey = "/policydb/service-accounts/"
|
||||
policyDBGroupsListKey = "/policydb/groups/"
|
||||
|
||||
allListKeys = []string{
|
||||
usersListKey,
|
||||
svcAccListKey,
|
||||
groupsListKey,
|
||||
policiesListKey,
|
||||
stsListKey,
|
||||
policyDBUsersListKey,
|
||||
policyDBSTSUsersListKey,
|
||||
policyDBServiceAccountsListKey,
|
||||
policyDBGroupsListKey,
|
||||
}
|
||||
)
|
||||
|
||||
func (iamOS *IAMObjectStore) listAllIAMConfigItems(ctx context.Context) (map[string][]string, error) {
|
||||
res := make(map[string][]string)
|
||||
|
||||
for item := range listIAMConfigItems(ctx, iamOS.objAPI, iamConfigPrefix) {
|
||||
if item.Err != nil {
|
||||
return nil, item.Err
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, listKey := range allListKeys {
|
||||
if strings.HasPrefix(item.Item, listKey) {
|
||||
found = true
|
||||
name := strings.TrimPrefix(item.Item, listKey)
|
||||
res[listKey] = append(res[listKey], name)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found && !(item.Item == "config/config.json" || item.Item == "/format.json") {
|
||||
logger.LogIf(ctx, fmt.Errorf("unknown type of IAM file listed: %v", item.Item))
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// Assumes cache is locked by caller.
|
||||
func (iamOS *IAMObjectStore) loadAllFromObjStore(ctx context.Context, cache *iamCache) error {
|
||||
listedConfigItems, err := iamOS.listAllIAMConfigItems(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Loads things in the same order as `LoadIAMCache()`
|
||||
|
||||
policiesList := listedConfigItems[policiesListKey]
|
||||
for _, item := range policiesList {
|
||||
policyName := path.Dir(item)
|
||||
if err := iamOS.loadPolicyDoc(ctx, policyName, cache.iamPolicyDocsMap); err != nil && err != errNoSuchPolicy {
|
||||
return err
|
||||
}
|
||||
}
|
||||
setDefaultCannedPolicies(cache.iamPolicyDocsMap)
|
||||
|
||||
if iamOS.usersSysType == MinIOUsersSysType {
|
||||
|
||||
regUsersList := listedConfigItems[usersListKey]
|
||||
for _, item := range regUsersList {
|
||||
userName := path.Dir(item)
|
||||
if err := iamOS.loadUser(ctx, userName, regUser, cache.iamUsersMap); err != nil && err != errNoSuchUser {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
groupsList := listedConfigItems[groupsListKey]
|
||||
for _, item := range groupsList {
|
||||
group := path.Dir(item)
|
||||
if err := iamOS.loadGroup(ctx, group, cache.iamGroupsMap); err != nil && err != errNoSuchGroup {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
userPolicyMappingsList := listedConfigItems[policyDBUsersListKey]
|
||||
for _, item := range userPolicyMappingsList {
|
||||
userName := strings.TrimSuffix(item, ".json")
|
||||
if err := iamOS.loadMappedPolicy(ctx, userName, regUser, false, cache.iamUserPolicyMap); err != nil && err != errNoSuchPolicy {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
groupPolicyMappingsList := listedConfigItems[policyDBGroupsListKey]
|
||||
for _, item := range groupPolicyMappingsList {
|
||||
groupName := strings.TrimSuffix(item, ".json")
|
||||
if err := iamOS.loadMappedPolicy(ctx, groupName, regUser, true, cache.iamGroupPolicyMap); err != nil && err != errNoSuchPolicy {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
svcAccList := listedConfigItems[svcAccListKey]
|
||||
for _, item := range svcAccList {
|
||||
userName := path.Dir(item)
|
||||
if err := iamOS.loadUser(ctx, userName, svcUser, cache.iamUsersMap); err != nil && err != errNoSuchUser {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
stsUsersList := listedConfigItems[stsListKey]
|
||||
for _, item := range stsUsersList {
|
||||
userName := path.Dir(item)
|
||||
if err := iamOS.loadUser(ctx, userName, stsUser, cache.iamUsersMap); err != nil && err != errNoSuchUser {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
stsPolicyMappingsList := listedConfigItems[policyDBSTSUsersListKey]
|
||||
for _, item := range stsPolicyMappingsList {
|
||||
stsName := strings.TrimSuffix(item, ".json")
|
||||
if err := iamOS.loadMappedPolicy(ctx, stsName, stsUser, false, cache.iamUserPolicyMap); err != nil && err != errNoSuchPolicy {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
cache.buildUserGroupMemberships()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (iamOS *IAMObjectStore) savePolicyDoc(ctx context.Context, policyName string, p PolicyDoc) error {
|
||||
return iamOS.saveIAMConfig(ctx, &p, getPolicyDocPath(policyName))
|
||||
}
|
||||
|
||||
+78
-47
@@ -432,48 +432,56 @@ func (store *IAMStoreSys) LoadIAMCache(ctx context.Context) error {
|
||||
cache := store.lock()
|
||||
defer store.unlock()
|
||||
|
||||
if err := store.loadPolicyDocs(ctx, newCache.iamPolicyDocsMap); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Sets default canned policies, if none are set.
|
||||
setDefaultCannedPolicies(newCache.iamPolicyDocsMap)
|
||||
|
||||
if store.getUsersSysType() == MinIOUsersSysType {
|
||||
if err := store.loadUsers(ctx, regUser, newCache.iamUsersMap); err != nil {
|
||||
if iamOS, ok := store.IAMStorageAPI.(*IAMObjectStore); ok {
|
||||
err := iamOS.loadAllFromObjStore(ctx, newCache)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := store.loadGroups(ctx, newCache.iamGroupsMap); err != nil {
|
||||
} else {
|
||||
|
||||
if err := store.loadPolicyDocs(ctx, newCache.iamPolicyDocsMap); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// load polices mapped to users
|
||||
if err := store.loadMappedPolicies(ctx, regUser, false, newCache.iamUserPolicyMap); err != nil {
|
||||
return err
|
||||
}
|
||||
// Sets default canned policies, if none are set.
|
||||
setDefaultCannedPolicies(newCache.iamPolicyDocsMap)
|
||||
|
||||
// load policies mapped to groups
|
||||
if err := store.loadMappedPolicies(ctx, regUser, true, newCache.iamGroupPolicyMap); err != nil {
|
||||
return err
|
||||
}
|
||||
if store.getUsersSysType() == MinIOUsersSysType {
|
||||
if err := store.loadUsers(ctx, regUser, newCache.iamUsersMap); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := store.loadGroups(ctx, newCache.iamGroupsMap); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// load service accounts
|
||||
if err := store.loadUsers(ctx, svcUser, newCache.iamUsersMap); err != nil {
|
||||
return err
|
||||
}
|
||||
// load polices mapped to users
|
||||
if err := store.loadMappedPolicies(ctx, regUser, false, newCache.iamUserPolicyMap); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// load STS temp users
|
||||
if err := store.loadUsers(ctx, stsUser, newCache.iamUsersMap); err != nil {
|
||||
return err
|
||||
}
|
||||
// load policies mapped to groups
|
||||
if err := store.loadMappedPolicies(ctx, regUser, true, newCache.iamGroupPolicyMap); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// load STS policy mappings
|
||||
if err := store.loadMappedPolicies(ctx, stsUser, false, newCache.iamUserPolicyMap); err != nil {
|
||||
return err
|
||||
}
|
||||
// load service accounts
|
||||
if err := store.loadUsers(ctx, svcUser, newCache.iamUsersMap); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newCache.buildUserGroupMemberships()
|
||||
// load STS temp users
|
||||
if err := store.loadUsers(ctx, stsUser, newCache.iamUsersMap); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// load STS policy mappings
|
||||
if err := store.loadMappedPolicies(ctx, stsUser, false, newCache.iamUserPolicyMap); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newCache.buildUserGroupMemberships()
|
||||
}
|
||||
|
||||
cache.iamGroupPolicyMap = newCache.iamGroupPolicyMap
|
||||
cache.iamGroupsMap = newCache.iamGroupsMap
|
||||
@@ -1456,13 +1464,28 @@ func (store *IAMStoreSys) GetAllParentUsers() map[string]string {
|
||||
|
||||
res := map[string]string{}
|
||||
for _, cred := range cache.iamUsersMap {
|
||||
if cred.IsServiceAccount() || cred.IsTemp() {
|
||||
if (cred.IsServiceAccount() || cred.IsTemp()) && cred.SessionToken != "" {
|
||||
parentUser := cred.ParentUser
|
||||
if cred.SessionToken != "" {
|
||||
claims, err := getClaimsFromToken(cred.SessionToken)
|
||||
|
||||
var (
|
||||
err error
|
||||
claims map[string]interface{}
|
||||
)
|
||||
|
||||
if cred.IsServiceAccount() {
|
||||
claims, err = getClaimsFromTokenWithSecret(cred.SessionToken, cred.SecretKey)
|
||||
if err != nil {
|
||||
continue
|
||||
claims, err = getClaimsFromTokenWithSecret(cred.SessionToken, globalActiveCred.SecretKey)
|
||||
}
|
||||
} else if cred.IsTemp() {
|
||||
claims, err = getClaimsFromTokenWithSecret(cred.SessionToken, globalActiveCred.SecretKey)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(claims) > 0 {
|
||||
if v, ok := claims[subClaim]; ok {
|
||||
subFromToken, ok := v.(string)
|
||||
if ok {
|
||||
@@ -1470,6 +1493,11 @@ func (store *IAMStoreSys) GetAllParentUsers() map[string]string {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if parentUser == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := res[parentUser]; !ok {
|
||||
res[parentUser] = cred.ParentUser
|
||||
}
|
||||
@@ -1561,6 +1589,7 @@ func (store *IAMStoreSys) UpdateServiceAccount(ctx context.Context, accessKey st
|
||||
return errNoSuchServiceAccount
|
||||
}
|
||||
|
||||
currentSecretKey := cr.SecretKey
|
||||
if opts.secretKey != "" {
|
||||
if !auth.IsSecretKeyValid(opts.secretKey) {
|
||||
return auth.ErrInvalidSecretKeyLength
|
||||
@@ -1582,20 +1611,21 @@ func (store *IAMStoreSys) UpdateServiceAccount(ctx context.Context, accessKey st
|
||||
return errors.New("unknown account status value")
|
||||
}
|
||||
|
||||
if opts.sessionPolicy != nil {
|
||||
m, err := getClaimsFromToken(cr.SessionToken)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to get svc acc claims: %v", err)
|
||||
}
|
||||
m, err := getClaimsFromTokenWithSecret(cr.SessionToken, currentSecretKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to get svc acc claims: %v", err)
|
||||
}
|
||||
|
||||
err = opts.sessionPolicy.Validate()
|
||||
if err != nil {
|
||||
if opts.sessionPolicy != nil {
|
||||
if err := opts.sessionPolicy.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
policyBuf, err := json.Marshal(opts.sessionPolicy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(policyBuf) > 16*humanize.KiByte {
|
||||
return fmt.Errorf("Session policy should not exceed 16 KiB characters")
|
||||
}
|
||||
@@ -1603,10 +1633,11 @@ func (store *IAMStoreSys) UpdateServiceAccount(ctx context.Context, accessKey st
|
||||
// Overwrite session policy claims.
|
||||
m[iampolicy.SessionPolicyName] = base64.StdEncoding.EncodeToString(policyBuf)
|
||||
m[iamPolicyClaimNameSA()] = "embedded-policy"
|
||||
cr.SessionToken, err = auth.JWTSignWithAccessKey(accessKey, m, globalActiveCred.SecretKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
cr.SessionToken, err = auth.JWTSignWithAccessKey(accessKey, m, cr.SecretKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u := newUserIdentity(cr)
|
||||
|
||||
+57
-14
@@ -37,6 +37,7 @@ import (
|
||||
"github.com/minio/minio/internal/arn"
|
||||
"github.com/minio/minio/internal/auth"
|
||||
"github.com/minio/minio/internal/color"
|
||||
"github.com/minio/minio/internal/jwt"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
iampolicy "github.com/minio/pkg/iam/policy"
|
||||
etcd "go.etcd.io/etcd/client/v3"
|
||||
@@ -198,6 +199,8 @@ func (sys *IAMSys) Load(ctx context.Context) error {
|
||||
|
||||
// Init - initializes config system by reading entries from config/iam
|
||||
func (sys *IAMSys) Init(ctx context.Context, objAPI ObjectLayer, etcdClient *etcd.Client, iamRefreshInterval time.Duration) {
|
||||
iamInitStart := time.Now()
|
||||
|
||||
sys.Lock()
|
||||
defer sys.Unlock()
|
||||
|
||||
@@ -268,6 +271,8 @@ func (sys *IAMSys) Init(ctx context.Context, objAPI ObjectLayer, etcdClient *etc
|
||||
break
|
||||
}
|
||||
|
||||
iamLoadStart := time.Now()
|
||||
|
||||
// Load IAM data from storage.
|
||||
for {
|
||||
if err := sys.Load(retryCtx); err != nil {
|
||||
@@ -332,6 +337,9 @@ func (sys *IAMSys) Init(ctx context.Context, objAPI ObjectLayer, etcdClient *etc
|
||||
}
|
||||
|
||||
sys.printIAMRoles()
|
||||
|
||||
now := time.Now()
|
||||
logger.Info("Finished loading IAM sub-system (took %.1fs of %.1fs to load data).", now.Sub(iamLoadStart).Seconds(), now.Sub(iamInitStart).Seconds())
|
||||
}
|
||||
|
||||
// Prints IAM role ARNs.
|
||||
@@ -365,20 +373,30 @@ func (sys *IAMSys) watch(ctx context.Context) {
|
||||
for event := range ch {
|
||||
// we simply log errors
|
||||
err := sys.loadWatchedEvent(ctx, event)
|
||||
logger.LogIf(ctx, err)
|
||||
logger.LogIf(ctx, fmt.Errorf("Failure in loading watch event: %v", err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var maxRefreshDurationSecondsForLog float64 = 10
|
||||
|
||||
// Fall back to loading all items periodically
|
||||
ticker := time.NewTicker(sys.iamRefreshInterval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
refreshStart := time.Now()
|
||||
if err := sys.Load(ctx); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
logger.LogIf(ctx, fmt.Errorf("Failure in periodic refresh for IAM (took %.2fs): %v", time.Since(refreshStart).Seconds(), err))
|
||||
} else {
|
||||
took := time.Since(refreshStart).Seconds()
|
||||
if took > maxRefreshDurationSecondsForLog {
|
||||
// Log if we took a lot of time to load.
|
||||
logger.Info("IAM refresh took %.2fs", took)
|
||||
}
|
||||
}
|
||||
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
@@ -799,14 +817,17 @@ func (sys *IAMSys) NewServiceAccount(ctx context.Context, parentUser string, gro
|
||||
}
|
||||
}
|
||||
|
||||
var cred auth.Credentials
|
||||
|
||||
var accessKey, secretKey string
|
||||
var err error
|
||||
if len(opts.accessKey) > 0 {
|
||||
cred, err = auth.CreateNewCredentialsWithMetadata(opts.accessKey, opts.secretKey, m, globalActiveCred.SecretKey)
|
||||
accessKey, secretKey = opts.accessKey, opts.secretKey
|
||||
} else {
|
||||
cred, err = auth.GetNewCredentialsWithMetadata(m, globalActiveCred.SecretKey)
|
||||
accessKey, secretKey, err = auth.GenerateCredentials()
|
||||
if err != nil {
|
||||
return auth.Credentials{}, err
|
||||
}
|
||||
}
|
||||
cred, err := auth.CreateNewCredentialsWithMetadata(accessKey, secretKey, m, secretKey)
|
||||
if err != nil {
|
||||
return auth.Credentials{}, err
|
||||
}
|
||||
@@ -880,9 +901,12 @@ func (sys *IAMSys) getServiceAccount(ctx context.Context, accessKey string) (aut
|
||||
|
||||
var embeddedPolicy *iampolicy.Policy
|
||||
|
||||
jwtClaims, err := auth.ExtractClaims(sa.SessionToken, globalActiveCred.SecretKey)
|
||||
jwtClaims, err := auth.ExtractClaims(sa.SessionToken, sa.SecretKey)
|
||||
if err != nil {
|
||||
return auth.Credentials{}, nil, err
|
||||
jwtClaims, err = auth.ExtractClaims(sa.SessionToken, globalActiveCred.SecretKey)
|
||||
if err != nil {
|
||||
return auth.Credentials{}, nil, err
|
||||
}
|
||||
}
|
||||
pt, ptok := jwtClaims.Lookup(iamPolicyClaimNameSA())
|
||||
sp, spok := jwtClaims.Lookup(iampolicy.SessionPolicyName)
|
||||
@@ -915,9 +939,12 @@ func (sys *IAMSys) GetClaimsForSvcAcc(ctx context.Context, accessKey string) (ma
|
||||
return nil, errNoSuchServiceAccount
|
||||
}
|
||||
|
||||
jwtClaims, err := auth.ExtractClaims(sa.SessionToken, globalActiveCred.SecretKey)
|
||||
jwtClaims, err := auth.ExtractClaims(sa.SessionToken, sa.SecretKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
jwtClaims, err = auth.ExtractClaims(sa.SessionToken, globalActiveCred.SecretKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return jwtClaims.Map(), nil
|
||||
}
|
||||
@@ -1063,12 +1090,28 @@ func (sys *IAMSys) updateGroupMembershipsForLDAP(ctx context.Context) {
|
||||
if _, ok := parentUserToCredsMap[cred.ParentUser]; !ok {
|
||||
// Try to find the ldapUsername for this
|
||||
// parentUser by extracting JWT claims
|
||||
jwtClaims, err := auth.ExtractClaims(cred.SessionToken, globalActiveCred.SecretKey)
|
||||
if err != nil {
|
||||
// skip this cred - session token seems
|
||||
// invalid
|
||||
var (
|
||||
jwtClaims *jwt.MapClaims
|
||||
err error
|
||||
)
|
||||
|
||||
if cred.SessionToken == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if cred.IsServiceAccount() {
|
||||
jwtClaims, err = auth.ExtractClaims(cred.SessionToken, cred.SecretKey)
|
||||
if err != nil {
|
||||
jwtClaims, err = auth.ExtractClaims(cred.SessionToken, globalActiveCred.SecretKey)
|
||||
}
|
||||
} else {
|
||||
jwtClaims, err = auth.ExtractClaims(cred.SessionToken, globalActiveCred.SecretKey)
|
||||
}
|
||||
if err != nil {
|
||||
// skip this cred - session token seems invalid
|
||||
continue
|
||||
}
|
||||
|
||||
ldapUsername, ok := jwtClaims.Lookup(ldapUserN)
|
||||
if !ok {
|
||||
// skip this cred - we dont have the
|
||||
|
||||
+1
-1
@@ -132,7 +132,7 @@ func authenticateURL(accessKey, secretKey string) (string, error) {
|
||||
// Check if the request is authenticated.
|
||||
// Returns nil if the request is authenticated. errNoAuthToken if token missing.
|
||||
// Returns errAuthentication for all other errors.
|
||||
func webRequestAuthenticate(req *http.Request) (*xjwt.MapClaims, []string, bool, error) {
|
||||
func metricsRequestAuthenticate(req *http.Request) (*xjwt.MapClaims, []string, bool, error) {
|
||||
token, err := jwtreq.AuthorizationHeaderExtractor.ExtractToken(req)
|
||||
if err != nil {
|
||||
if err == jwtreq.ErrNoTokenInRequest {
|
||||
|
||||
+1
-1
@@ -149,7 +149,7 @@ func TestWebRequestAuthenticate(t *testing.T) {
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
_, _, _, gotErr := webRequestAuthenticate(testCase.req)
|
||||
_, _, _, gotErr := metricsRequestAuthenticate(testCase.req)
|
||||
if testCase.expectedErr != gotErr {
|
||||
t.Errorf("Test %d, expected err %s, got %s", i+1, testCase.expectedErr, gotErr)
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ func Benchmark_bucketMetacache_findCache(b *testing.B) {
|
||||
FilterPrefix: "",
|
||||
Marker: "",
|
||||
Limit: 0,
|
||||
AskDisks: 0,
|
||||
AskDisks: "strict",
|
||||
Recursive: false,
|
||||
Separator: slashSeparator,
|
||||
Create: true,
|
||||
@@ -59,7 +59,7 @@ func Benchmark_bucketMetacache_findCache(b *testing.B) {
|
||||
FilterPrefix: "",
|
||||
Marker: "",
|
||||
Limit: 0,
|
||||
AskDisks: 0,
|
||||
AskDisks: "strict",
|
||||
Recursive: false,
|
||||
Separator: slashSeparator,
|
||||
Create: true,
|
||||
|
||||
@@ -281,10 +281,16 @@ func (m metaCacheEntries) shallowClone() metaCacheEntries {
|
||||
}
|
||||
|
||||
type metadataResolutionParams struct {
|
||||
dirQuorum int // Number if disks needed for a directory to 'exist'.
|
||||
objQuorum int // Number of disks needed for an object to 'exist'.
|
||||
bucket string // Name of the bucket. Used for generating cached fileinfo.
|
||||
strict bool // Versions must match exactly, including all metadata.
|
||||
dirQuorum int // Number if disks needed for a directory to 'exist'.
|
||||
objQuorum int // Number of disks needed for an object to 'exist'.
|
||||
|
||||
// An optimization request only an 'n' amount of versions from xl.meta
|
||||
// to avoid resolving all versions to figure out the latest 'version'
|
||||
// for ListObjects, ListObjectsV2
|
||||
requestedVersions int
|
||||
|
||||
bucket string // Name of the bucket. Used for generating cached fileinfo.
|
||||
strict bool // Versions must match exactly, including all metadata.
|
||||
|
||||
// Reusable slice for resolution
|
||||
candidates [][]xlMetaV2ShallowVersion
|
||||
@@ -372,7 +378,7 @@ func (m metaCacheEntries) resolve(r *metadataResolutionParams) (selected *metaCa
|
||||
reusable: true,
|
||||
cached: &xlMetaV2{metaV: selected.cached.metaV},
|
||||
}
|
||||
selected.cached.versions = mergeXLV2Versions(r.objQuorum, r.strict, r.candidates...)
|
||||
selected.cached.versions = mergeXLV2Versions(r.objQuorum, r.strict, r.requestedVersions, r.candidates...)
|
||||
if len(selected.cached.versions) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
@@ -498,7 +504,7 @@ func (m *metaCacheEntriesSorted) fileInfoVersions(bucket, prefix, delimiter, aft
|
||||
return versions
|
||||
}
|
||||
|
||||
// fileInfoVersions converts the metadata to FileInfoVersions where possible.
|
||||
// fileInfos converts the metadata to ObjectInfo where possible.
|
||||
// Metadata that cannot be decoded is skipped.
|
||||
func (m *metaCacheEntriesSorted) fileInfos(bucket, prefix, delimiter string) (objects []ObjectInfo) {
|
||||
objects = make([]ObjectInfo, 0, m.len())
|
||||
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/internal/bucket/lifecycle"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
)
|
||||
|
||||
@@ -110,7 +111,7 @@ func (z *erasureServerPools) listPath(ctx context.Context, o *listPathOptions) (
|
||||
// If we don't have a list id we must ask the server if it has a cache or create a new.
|
||||
if o.ID != "" && !o.Transient {
|
||||
// Create or ping with handout...
|
||||
rpc := globalNotificationSys.restClientFromHash(o.Bucket)
|
||||
rpc := globalNotificationSys.restClientFromHash(pathJoin(o.Bucket, o.Prefix))
|
||||
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
var c *metacache
|
||||
@@ -198,7 +199,7 @@ func (z *erasureServerPools) listPath(ctx context.Context, o *listPathOptions) (
|
||||
}
|
||||
entries.truncate(0)
|
||||
go func() {
|
||||
rpc := globalNotificationSys.restClientFromHash(o.Bucket)
|
||||
rpc := globalNotificationSys.restClientFromHash(pathJoin(o.Bucket, o.Prefix))
|
||||
if rpc != nil {
|
||||
ctx, cancel := context.WithTimeout(GlobalContext, 5*time.Second)
|
||||
defer cancel()
|
||||
@@ -289,6 +290,14 @@ func (z *erasureServerPools) listMerged(ctx context.Context, o listPathOptions,
|
||||
}
|
||||
mu.Unlock()
|
||||
|
||||
// Do lifecycle filtering.
|
||||
if o.lcFilter != nil {
|
||||
filterIn := make(chan metaCacheEntry, 10)
|
||||
go filterLifeCycle(ctx, o.Bucket, o.lcFilter, filterIn, results)
|
||||
// Replace results.
|
||||
results = filterIn
|
||||
}
|
||||
|
||||
// Gather results to a single channel.
|
||||
err := mergeEntryChannels(ctx, inputs, results, func(existing, other *metaCacheEntry) (replace bool) {
|
||||
// Pick object over directory
|
||||
@@ -298,21 +307,20 @@ func (z *erasureServerPools) listMerged(ctx context.Context, o listPathOptions,
|
||||
if !existing.isDir() && other.isDir() {
|
||||
return false
|
||||
}
|
||||
|
||||
eFIV, err := existing.fileInfo(o.Bucket)
|
||||
eMeta, err := existing.xlmeta()
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
oFIV, err := other.fileInfo(o.Bucket)
|
||||
oMeta, err := other.xlmeta()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
// Replace if modtime is newer
|
||||
if !oFIV.ModTime.Equal(eFIV.ModTime) {
|
||||
return oFIV.ModTime.After(eFIV.ModTime)
|
||||
if !oMeta.latestModtime().Equal(oMeta.latestModtime()) {
|
||||
return oMeta.latestModtime().After(eMeta.latestModtime())
|
||||
}
|
||||
// Use NumVersions as a final tiebreaker.
|
||||
return oFIV.NumVersions > eFIV.NumVersions
|
||||
return len(oMeta.versions) > len(eMeta.versions)
|
||||
})
|
||||
|
||||
cancelList()
|
||||
@@ -346,6 +354,44 @@ func (z *erasureServerPools) listMerged(ctx context.Context, o listPathOptions,
|
||||
return nil
|
||||
}
|
||||
|
||||
// filterLifeCycle will filter out objects if the most recent
|
||||
// version should be deleted by lifecycle.
|
||||
// out will be closed when there are no more results.
|
||||
// When 'in' is closed or the context is canceled the
|
||||
// function closes 'out' and exits.
|
||||
func filterLifeCycle(ctx context.Context, bucket string, lc *lifecycle.Lifecycle, in <-chan metaCacheEntry, out chan<- metaCacheEntry) {
|
||||
defer close(out)
|
||||
for {
|
||||
var obj metaCacheEntry
|
||||
var ok bool
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case obj, ok = <-in:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
fi, err := obj.fileInfo(bucket)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
objInfo := fi.ToObjectInfo(bucket, obj.name)
|
||||
action := evalActionFromLifecycle(ctx, *lc, objInfo, false)
|
||||
switch action {
|
||||
case lifecycle.DeleteVersionAction, lifecycle.DeleteAction:
|
||||
// Skip this entry.
|
||||
continue
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case out <- obj:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (z *erasureServerPools) listAndSave(ctx context.Context, o *listPathOptions) (entries metaCacheEntriesSorted, err error) {
|
||||
// Use ID as the object name...
|
||||
o.pool = z.getAvailablePoolIdx(ctx, minioMetaBucket, o.ID, 10<<20)
|
||||
@@ -369,7 +415,7 @@ func (z *erasureServerPools) listAndSave(ctx context.Context, o *listPathOptions
|
||||
filteredResults := o.gatherResults(ctx, outCh)
|
||||
|
||||
mc := o.newMetacache()
|
||||
meta := metaCacheRPC{meta: &mc, cancel: cancel, rpc: globalNotificationSys.restClientFromHash(o.Bucket), o: *o}
|
||||
meta := metaCacheRPC{meta: &mc, cancel: cancel, rpc: globalNotificationSys.restClientFromHash(pathJoin(o.Bucket, o.Prefix)), o: *o}
|
||||
|
||||
// Save listing...
|
||||
go func() {
|
||||
|
||||
+76
-31
@@ -32,6 +32,7 @@ import (
|
||||
"time"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/minio/minio/internal/bucket/lifecycle"
|
||||
"github.com/minio/minio/internal/color"
|
||||
"github.com/minio/minio/internal/hash"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
@@ -64,10 +65,8 @@ type listPathOptions struct {
|
||||
// Limit the number of results.
|
||||
Limit int
|
||||
|
||||
// The number of disks to ask. Special values:
|
||||
// 0 uses default number of disks.
|
||||
// -1 use at least 50% of disks or at least the default number.
|
||||
AskDisks int
|
||||
// The number of disks to ask.
|
||||
AskDisks string
|
||||
|
||||
// InclDeleted will keep all entries where latest version is a delete marker.
|
||||
InclDeleted bool
|
||||
@@ -96,6 +95,11 @@ type listPathOptions struct {
|
||||
|
||||
// pool and set of where the cache is located.
|
||||
pool, set int
|
||||
|
||||
// lcFilter performs filtering based on lifecycle.
|
||||
// This will filter out objects if the most recent version should be deleted by lifecycle.
|
||||
// Is not transferred across request calls.
|
||||
lcFilter *lifecycle.Lifecycle
|
||||
}
|
||||
|
||||
func init() {
|
||||
@@ -189,7 +193,11 @@ func (o *listPathOptions) gatherResults(ctx context.Context, in <-chan metaCache
|
||||
}
|
||||
if resCh != nil {
|
||||
resErr = io.EOF
|
||||
resCh <- results
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// Nobody wants it.
|
||||
case resCh <- results:
|
||||
}
|
||||
}
|
||||
}()
|
||||
return func() (metaCacheEntriesSorted, error) {
|
||||
@@ -351,7 +359,7 @@ func (r *metacacheReader) filter(o listPathOptions) (entries metaCacheEntriesSor
|
||||
|
||||
func (er *erasureObjects) streamMetadataParts(ctx context.Context, o listPathOptions) (entries metaCacheEntriesSorted, err error) {
|
||||
retries := 0
|
||||
rpc := globalNotificationSys.restClientFromHash(o.Bucket)
|
||||
rpc := globalNotificationSys.restClientFromHash(pathJoin(o.Bucket, o.Prefix))
|
||||
|
||||
for {
|
||||
if contextCanceled(ctx) {
|
||||
@@ -531,20 +539,39 @@ func (er *erasureObjects) streamMetadataParts(ctx context.Context, o listPathOpt
|
||||
}
|
||||
}
|
||||
|
||||
// getListQuorum interprets list quorum values and returns appropriate
|
||||
// acceptable quorum expected for list operations
|
||||
func getListQuorum(quorum string, driveCount int) int {
|
||||
switch quorum {
|
||||
case "disk":
|
||||
// smallest possible value, generally meant for testing.
|
||||
return 1
|
||||
case "reduced":
|
||||
return 2
|
||||
case "strict":
|
||||
return -1
|
||||
}
|
||||
// Defaults to (driveCount+1)/2 drives per set, defaults to "optimal" value
|
||||
if driveCount > 0 {
|
||||
return (driveCount + 1) / 2
|
||||
} // "3" otherwise.
|
||||
return 3
|
||||
}
|
||||
|
||||
// Will return io.EOF if continuing would not yield more results.
|
||||
func (er *erasureObjects) listPath(ctx context.Context, o listPathOptions, results chan<- metaCacheEntry) (err error) {
|
||||
defer close(results)
|
||||
o.debugf(color.Green("listPath:")+" with options: %#v", o)
|
||||
|
||||
askDisks := o.AskDisks
|
||||
listingQuorum := o.AskDisks - 1
|
||||
askDisks := getListQuorum(o.AskDisks, er.setDriveCount)
|
||||
listingQuorum := askDisks - 1
|
||||
disks := er.getDisks()
|
||||
var fallbackDisks []StorageAPI
|
||||
|
||||
// Special case: ask all disks if the drive count is 4
|
||||
if askDisks <= 0 || er.setDriveCount == 4 {
|
||||
askDisks = len(disks) // with 'strict' quorum list on all online disks.
|
||||
listingQuorum = len(disks) / 2 // keep this such that we can list all objects with different quorum ratio.
|
||||
askDisks = len(disks) // with 'strict' quorum list on all drives.
|
||||
listingQuorum = (len(disks) + 1) / 2 // keep this such that we can list all objects with different quorum ratio.
|
||||
}
|
||||
if askDisks > 0 && len(disks) > askDisks {
|
||||
rand.Shuffle(len(disks), func(i, j int) {
|
||||
@@ -561,6 +588,13 @@ func (er *erasureObjects) listPath(ctx context.Context, o listPathOptions, resul
|
||||
bucket: o.Bucket,
|
||||
}
|
||||
|
||||
// Maximum versions requested for "latest" object
|
||||
// resolution on versioned buckets, this is to be only
|
||||
// used when o.Versioned is false
|
||||
if !o.Versioned {
|
||||
resolver.requestedVersions = 1
|
||||
}
|
||||
|
||||
ctxDone := ctx.Done()
|
||||
return listPathRaw(ctx, listPathRawOptions{
|
||||
disks: disks,
|
||||
@@ -789,14 +823,30 @@ func listPathRaw(ctx context.Context, opts listPathRawOptions) (err error) {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
fallback := func(err error) bool {
|
||||
// Keep track of fallback disks
|
||||
var fdMu sync.Mutex
|
||||
fds := opts.fallbackDisks
|
||||
fallback := func(err error) StorageAPI {
|
||||
switch err.(type) {
|
||||
case StorageErr:
|
||||
// all supported disk errors
|
||||
// attempt a fallback.
|
||||
return true
|
||||
// Attempt to grab a fallback disk
|
||||
fdMu.Lock()
|
||||
defer fdMu.Unlock()
|
||||
if len(fds) == 0 {
|
||||
return nil
|
||||
}
|
||||
fdsCopy := fds
|
||||
for _, fd := range fdsCopy {
|
||||
// Grab a fallback disk
|
||||
fds = fds[1:]
|
||||
if fd != nil && fd.IsOnline() {
|
||||
return fd
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
// Either no more disks for fallback or
|
||||
// not a storage error.
|
||||
return nil
|
||||
}
|
||||
askDisks := len(disks)
|
||||
readers := make([]*metacacheReader, askDisks)
|
||||
@@ -825,25 +875,20 @@ func listPathRaw(ctx context.Context, opts listPathRawOptions) (err error) {
|
||||
}
|
||||
|
||||
// fallback only when set.
|
||||
if len(opts.fallbackDisks) > 0 && fallback(werr) {
|
||||
for fd := fallback(werr); fd != nil; {
|
||||
// This fallback is only set when
|
||||
// askDisks is less than total
|
||||
// number of disks per set.
|
||||
for _, fd := range opts.fallbackDisks {
|
||||
if fd == nil {
|
||||
continue
|
||||
}
|
||||
werr = fd.WalkDir(ctx, WalkDirOptions{
|
||||
Bucket: opts.bucket,
|
||||
BaseDir: opts.path,
|
||||
Recursive: opts.recursive,
|
||||
ReportNotFound: opts.reportNotFound,
|
||||
FilterPrefix: opts.filterPrefix,
|
||||
ForwardTo: opts.forwardTo,
|
||||
}, w)
|
||||
if werr == nil {
|
||||
break
|
||||
}
|
||||
werr = fd.WalkDir(ctx, WalkDirOptions{
|
||||
Bucket: opts.bucket,
|
||||
BaseDir: opts.path,
|
||||
Recursive: opts.recursive,
|
||||
ReportNotFound: opts.reportNotFound,
|
||||
FilterPrefix: opts.filterPrefix,
|
||||
ForwardTo: opts.forwardTo,
|
||||
}, w)
|
||||
if werr == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
w.CloseWithError(werr)
|
||||
|
||||
+3
-3
@@ -1651,13 +1651,13 @@ func getClusterTierMetrics() *MetricsGroup {
|
||||
cacheInterval: 10 * time.Second,
|
||||
}
|
||||
mg.RegisterRead(func(ctx context.Context) (metrics []Metric) {
|
||||
if globalTierConfigMgr.Empty() {
|
||||
return
|
||||
}
|
||||
objLayer := newObjectLayerFn()
|
||||
if objLayer == nil || globalIsGateway {
|
||||
return
|
||||
}
|
||||
if globalTierConfigMgr.Empty() {
|
||||
return
|
||||
}
|
||||
|
||||
dui, err := loadDataUsageFromBackend(GlobalContext, objLayer)
|
||||
if err != nil {
|
||||
|
||||
+1
-1
@@ -674,7 +674,7 @@ func metricsHandler() http.Handler {
|
||||
// AuthMiddleware checks if the bearer token is valid and authorized.
|
||||
func AuthMiddleware(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
claims, groups, owner, authErr := webRequestAuthenticate(r)
|
||||
claims, groups, owner, authErr := metricsRequestAuthenticate(r)
|
||||
if authErr != nil || !claims.VerifyIssuer("prometheus", true) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return
|
||||
|
||||
@@ -170,6 +170,10 @@ func (di *distLockInstance) GetLock(ctx context.Context, timeout *dynamicTimeout
|
||||
}) {
|
||||
timeout.LogFailure()
|
||||
cancel()
|
||||
switch err := newCtx.Err(); err {
|
||||
case context.Canceled:
|
||||
return LockContext{ctx: ctx, cancel: func() {}}, err
|
||||
}
|
||||
return LockContext{ctx: ctx, cancel: func() {}}, OperationTimedOut{}
|
||||
}
|
||||
timeout.LogSuccess(UTCNow().Sub(start))
|
||||
@@ -195,6 +199,10 @@ func (di *distLockInstance) GetRLock(ctx context.Context, timeout *dynamicTimeou
|
||||
}) {
|
||||
timeout.LogFailure()
|
||||
cancel()
|
||||
switch err := newCtx.Err(); err {
|
||||
case context.Canceled:
|
||||
return LockContext{ctx: ctx, cancel: func() {}}, err
|
||||
}
|
||||
return LockContext{ctx: ctx, cancel: func() {}}, OperationTimedOut{}
|
||||
}
|
||||
timeout.LogSuccess(UTCNow().Sub(start))
|
||||
@@ -247,6 +255,10 @@ func (li *localLockInstance) GetLock(ctx context.Context, timeout *dynamicTimeou
|
||||
li.ns.unlock(li.volume, li.paths[si], readLock)
|
||||
}
|
||||
}
|
||||
switch err := ctx.Err(); err {
|
||||
case context.Canceled:
|
||||
return LockContext{}, err
|
||||
}
|
||||
return LockContext{}, OperationTimedOut{}
|
||||
}
|
||||
success[i] = 1
|
||||
@@ -280,6 +292,10 @@ func (li *localLockInstance) GetRLock(ctx context.Context, timeout *dynamicTimeo
|
||||
li.ns.unlock(li.volume, li.paths[si], readLock)
|
||||
}
|
||||
}
|
||||
switch err := ctx.Err(); err {
|
||||
case context.Canceled:
|
||||
return LockContext{}, err
|
||||
}
|
||||
return LockContext{}, OperationTimedOut{}
|
||||
}
|
||||
success[i] = 1
|
||||
|
||||
@@ -703,5 +703,6 @@ func isErrMethodNotAllowed(err error) bool {
|
||||
}
|
||||
|
||||
func isErrInvalidRange(err error) bool {
|
||||
return errors.As(err, &errInvalidRange)
|
||||
_, ok := err.(InvalidRange)
|
||||
return ok
|
||||
}
|
||||
|
||||
+94
-93
@@ -206,6 +206,9 @@ func (api objectAPIHandlers) SelectObjectContentHandler(w http.ResponseWriter, r
|
||||
Start: offset,
|
||||
End: offset + length,
|
||||
}
|
||||
if length == -1 {
|
||||
rs.End = -1
|
||||
}
|
||||
|
||||
return getObjectNInfo(ctx, bucket, object, rs, r.Header, readLock, opts)
|
||||
}
|
||||
@@ -413,16 +416,17 @@ func (api objectAPIHandlers) getObjectHandler(ctx context.Context, objectAPI Obj
|
||||
var (
|
||||
reader *GetObjectReader
|
||||
proxy proxyResult
|
||||
perr error
|
||||
)
|
||||
proxytgts := getproxyTargets(ctx, bucket, object, opts)
|
||||
if !proxytgts.Empty() {
|
||||
// proxy to replication target if active-active replication is in place.
|
||||
reader, proxy, err = proxyGetToReplicationTarget(ctx, bucket, object, rs, r.Header, opts, proxytgts)
|
||||
if err != nil && !isErrObjectNotFound(ErrorRespToObjectError(err, bucket, object)) &&
|
||||
!isErrVersionNotFound(ErrorRespToObjectError(err, bucket, object)) {
|
||||
logger.LogIf(ctx, fmt.Errorf("Replication proxy failed for %s/%s(%s) - %w", bucket, object, opts.VersionID, err))
|
||||
reader, proxy, perr = proxyGetToReplicationTarget(ctx, bucket, object, rs, r.Header, opts, proxytgts)
|
||||
if perr != nil && !isErrObjectNotFound(ErrorRespToObjectError(perr, bucket, object)) &&
|
||||
!isErrVersionNotFound(ErrorRespToObjectError(perr, bucket, object)) {
|
||||
logger.LogIf(ctx, fmt.Errorf("Replication proxy failed for %s/%s(%s) - %w", bucket, object, opts.VersionID, perr))
|
||||
}
|
||||
if reader != nil && proxy.Proxy && err == nil {
|
||||
if reader != nil && proxy.Proxy && perr == nil {
|
||||
gr = reader
|
||||
}
|
||||
}
|
||||
@@ -2900,12 +2904,20 @@ func (api objectAPIHandlers) PutObjectPartHandler(w http.ResponseWriter, r *http
|
||||
}
|
||||
|
||||
etag := partInfo.ETag
|
||||
switch kind, encrypted := crypto.IsEncrypted(mi.UserDefined); {
|
||||
case encrypted:
|
||||
if kind, encrypted := crypto.IsEncrypted(mi.UserDefined); encrypted {
|
||||
switch kind {
|
||||
case crypto.S3KMS:
|
||||
w.Header().Set(xhttp.AmzServerSideEncryption, xhttp.AmzEncryptionKMS)
|
||||
w.Header().Set(xhttp.AmzServerSideEncryptionKmsID, mi.KMSKeyID())
|
||||
if kmsCtx, ok := mi.UserDefined[crypto.MetaContext]; ok {
|
||||
w.Header().Set(xhttp.AmzServerSideEncryptionKmsContext, kmsCtx)
|
||||
}
|
||||
if len(etag) >= 32 && strings.Count(etag, "-") != 1 {
|
||||
etag = etag[len(etag)-32:]
|
||||
}
|
||||
case crypto.S3:
|
||||
w.Header().Set(xhttp.AmzServerSideEncryption, xhttp.AmzEncryptionAES)
|
||||
etag = tryDecryptETag(objectEncryptionKey[:], etag, false)
|
||||
etag, _ = DecryptETag(objectEncryptionKey, ObjectInfo{ETag: etag})
|
||||
case crypto.SSEC:
|
||||
w.Header().Set(xhttp.AmzServerSideEncryptionCustomerAlgorithm, r.Header.Get(xhttp.AmzServerSideEncryptionCustomerAlgorithm))
|
||||
w.Header().Set(xhttp.AmzServerSideEncryptionCustomerKeyMD5, r.Header.Get(xhttp.AmzServerSideEncryptionCustomerKeyMD5))
|
||||
@@ -3013,34 +3025,29 @@ func (api objectAPIHandlers) ListObjectPartsHandler(w http.ResponseWriter, r *ht
|
||||
return
|
||||
}
|
||||
|
||||
var ssec bool
|
||||
if _, ok := crypto.IsEncrypted(listPartsInfo.UserDefined); ok && objectAPI.IsEncryptionSupported() {
|
||||
var key []byte
|
||||
if crypto.SSEC.IsEncrypted(listPartsInfo.UserDefined) {
|
||||
ssec = true
|
||||
}
|
||||
// We have to adjust the size of encrypted parts since encrypted parts
|
||||
// are slightly larger due to encryption overhead.
|
||||
// Further, we have to adjust the ETags of parts when using SSE-S3.
|
||||
// Due to AWS S3, SSE-S3 encrypted parts return the plaintext ETag
|
||||
// being the content MD5 of that particular part. This is not the
|
||||
// case for SSE-C and SSE-KMS objects.
|
||||
if kind, ok := crypto.IsEncrypted(listPartsInfo.UserDefined); ok && objectAPI.IsEncryptionSupported() {
|
||||
var objectEncryptionKey []byte
|
||||
if crypto.S3.IsEncrypted(listPartsInfo.UserDefined) {
|
||||
// Calculating object encryption key
|
||||
objectEncryptionKey, err = decryptObjectInfo(key, bucket, object, listPartsInfo.UserDefined)
|
||||
if kind == crypto.S3 {
|
||||
objectEncryptionKey, err = decryptObjectInfo(nil, bucket, object, listPartsInfo.UserDefined)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
for i := range listPartsInfo.Parts {
|
||||
curp := listPartsInfo.Parts[i]
|
||||
curp.ETag = tryDecryptETag(objectEncryptionKey, curp.ETag, ssec)
|
||||
if !ssec {
|
||||
var partSize uint64
|
||||
partSize, err = sio.DecryptedSize(uint64(curp.Size))
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
curp.Size = int64(partSize)
|
||||
for i, p := range listPartsInfo.Parts {
|
||||
listPartsInfo.Parts[i].ETag = tryDecryptETag(objectEncryptionKey, p.ETag, kind != crypto.S3)
|
||||
size, err := sio.DecryptedSize(uint64(p.Size))
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
listPartsInfo.Parts[i] = curp
|
||||
listPartsInfo.Parts[i].Size = int64(size)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3172,66 +3179,6 @@ func (api objectAPIHandlers) CompleteMultipartUploadHandler(w http.ResponseWrite
|
||||
return
|
||||
}
|
||||
|
||||
var objectEncryptionKey []byte
|
||||
var isEncrypted, ssec bool
|
||||
if objectAPI.IsEncryptionSupported() {
|
||||
mi, err := objectAPI.GetMultipartInfo(ctx, bucket, object, uploadID, ObjectOptions{})
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
if _, ok := crypto.IsEncrypted(mi.UserDefined); ok {
|
||||
var key []byte
|
||||
isEncrypted = true
|
||||
ssec = crypto.SSEC.IsEncrypted(mi.UserDefined)
|
||||
if crypto.S3.IsEncrypted(mi.UserDefined) {
|
||||
// Calculating object encryption key
|
||||
objectEncryptionKey, err = decryptObjectInfo(key, bucket, object, mi.UserDefined)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
partsMap := make(map[string]PartInfo)
|
||||
if isEncrypted {
|
||||
maxParts := 10000
|
||||
listPartsInfo, err := objectAPI.ListObjectParts(ctx, bucket, object, uploadID, 0, maxParts, ObjectOptions{})
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
for _, part := range listPartsInfo.Parts {
|
||||
partsMap[strconv.Itoa(part.PartNumber)] = part
|
||||
}
|
||||
}
|
||||
|
||||
// Complete parts.
|
||||
completeParts := make([]CompletePart, 0, len(complMultipartUpload.Parts))
|
||||
originalCompleteParts := make([]CompletePart, 0, len(complMultipartUpload.Parts))
|
||||
for _, part := range complMultipartUpload.Parts {
|
||||
part.ETag = canonicalizeETag(part.ETag)
|
||||
originalCompleteParts = append(originalCompleteParts, part)
|
||||
if isEncrypted {
|
||||
// ETag is stored in the backend in encrypted form. Validate client sent ETag with
|
||||
// decrypted ETag.
|
||||
if bkPartInfo, ok := partsMap[strconv.Itoa(part.PartNumber)]; ok {
|
||||
bkETag := tryDecryptETag(objectEncryptionKey, bkPartInfo.ETag, ssec)
|
||||
if bkETag != part.ETag {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidPart), r.URL)
|
||||
return
|
||||
}
|
||||
part.ETag = bkPartInfo.ETag
|
||||
}
|
||||
}
|
||||
completeParts = append(completeParts, part)
|
||||
}
|
||||
|
||||
// Calculate s3 compatible md5sum for complete multipart.
|
||||
s3MD5 := getCompleteMultipartMD5(originalCompleteParts)
|
||||
|
||||
completeMultiPartUpload := objectAPI.CompleteMultipartUpload
|
||||
if api.CacheAPI() != nil {
|
||||
completeMultiPartUpload = api.CacheAPI().CompleteMultipartUpload
|
||||
@@ -3274,14 +3221,68 @@ func (api objectAPIHandlers) CompleteMultipartUploadHandler(w http.ResponseWrite
|
||||
return
|
||||
}
|
||||
|
||||
// preserve ETag if set, or set from parts.
|
||||
if _, ok := opts.UserDefined["etag"]; !ok {
|
||||
opts.UserDefined["etag"] = s3MD5
|
||||
// First, we compute the ETag of the multipart object.
|
||||
// The ETag of a multi-part object is always:
|
||||
// ETag := MD5(ETag_p1, ETag_p2, ...)+"-N" (N being the number of parts)
|
||||
//
|
||||
// This is independent of encryption. An encrypted multipart
|
||||
// object also has an ETag that is the MD5 of its part ETags.
|
||||
// The fact the in case of encryption the ETag of a part is
|
||||
// not the MD5 of the part content does not change that.
|
||||
var completeETags []etag.ETag
|
||||
for _, part := range complMultipartUpload.Parts {
|
||||
ETag, err := etag.Parse(part.ETag)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
completeETags = append(completeETags, ETag)
|
||||
}
|
||||
multipartETag := etag.Multipart(completeETags...)
|
||||
opts.UserDefined["etag"] = multipartETag.String()
|
||||
|
||||
// However, in case of encryption, the persisted part ETags don't match
|
||||
// what we have sent to the client during PutObjectPart. The reason is
|
||||
// that ETags are encrypted. Hence, the client will send a list of complete
|
||||
// part ETags of which non can match the ETag of any part. For example
|
||||
// ETag (client): 30902184f4e62dd8f98f0aaff810c626
|
||||
// ETag (server-internal): 20000f00ce5dc16e3f3b124f586ae1d88e9caa1c598415c2759bbb50e84a59f630902184f4e62dd8f98f0aaff810c626
|
||||
//
|
||||
// Therefore, we adjust all ETags sent by the client to match what is stored
|
||||
// on the backend.
|
||||
if objectAPI.IsEncryptionSupported() {
|
||||
mi, err := objectAPI.GetMultipartInfo(ctx, bucket, object, uploadID, ObjectOptions{})
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if _, ok := crypto.IsEncrypted(mi.UserDefined); ok {
|
||||
const MaxParts = 10000
|
||||
listPartsInfo, err := objectAPI.ListObjectParts(ctx, bucket, object, uploadID, 0, MaxParts, ObjectOptions{})
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
sort.Slice(listPartsInfo.Parts, func(i, j int) bool {
|
||||
return listPartsInfo.Parts[i].PartNumber < listPartsInfo.Parts[j].PartNumber
|
||||
})
|
||||
sort.Slice(complMultipartUpload.Parts, func(i, j int) bool {
|
||||
return complMultipartUpload.Parts[i].PartNumber < complMultipartUpload.Parts[j].PartNumber
|
||||
})
|
||||
for i := range listPartsInfo.Parts {
|
||||
for j := range complMultipartUpload.Parts {
|
||||
if listPartsInfo.Parts[i].PartNumber == complMultipartUpload.Parts[j].PartNumber {
|
||||
complMultipartUpload.Parts[j].ETag = listPartsInfo.Parts[i].ETag
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
w = &whiteSpaceWriter{ResponseWriter: w, Flusher: w.(http.Flusher)}
|
||||
completeDoneCh := sendWhiteSpace(w)
|
||||
objInfo, err := completeMultiPartUpload(ctx, bucket, object, uploadID, completeParts, opts)
|
||||
objInfo, err := completeMultiPartUpload(ctx, bucket, object, uploadID, complMultipartUpload.Parts, opts)
|
||||
// Stop writing white spaces to the client. Note that close(doneCh) style is not used as it
|
||||
// can cause white space to be written after we send XML response in a race condition.
|
||||
headerWritten := <-completeDoneCh
|
||||
|
||||
+1
-24
@@ -19,7 +19,6 @@ package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
@@ -121,31 +120,9 @@ func isServerResolvable(endpoint Endpoint, timeout time.Duration) error {
|
||||
Path: pathJoin(healthCheckPathPrefix, healthCheckLivenessPath),
|
||||
}
|
||||
|
||||
var tlsConfig *tls.Config
|
||||
if globalIsTLS {
|
||||
tlsConfig = &tls.Config{
|
||||
RootCAs: globalRootCAs,
|
||||
}
|
||||
}
|
||||
|
||||
httpClient := &http.Client{
|
||||
Transport:
|
||||
// For more details about various values used here refer
|
||||
// https://golang.org/pkg/net/http/#Transport documentation
|
||||
&http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: xhttp.NewCustomDialContext(3 * time.Second),
|
||||
ResponseHeaderTimeout: 3 * time.Second,
|
||||
TLSHandshakeTimeout: 3 * time.Second,
|
||||
ExpectContinueTimeout: 3 * time.Second,
|
||||
TLSClientConfig: tlsConfig,
|
||||
// Go net/http automatically unzip if content-type is
|
||||
// gzip disable this feature, as we are always interested
|
||||
// in raw stream.
|
||||
DisableCompression: true,
|
||||
},
|
||||
Transport: globalInternodeTransport,
|
||||
}
|
||||
defer httpClient.CloseIdleConnections()
|
||||
|
||||
ctx, cancel := context.WithTimeout(GlobalContext, timeout)
|
||||
|
||||
|
||||
+10
-1
@@ -28,6 +28,7 @@ import (
|
||||
"math/rand"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
@@ -299,6 +300,8 @@ func configRetriableErrors(err error) bool {
|
||||
}
|
||||
|
||||
func initServer(ctx context.Context, newObject ObjectLayer) error {
|
||||
t1 := time.Now()
|
||||
|
||||
// Once the config is fully loaded, initialize the new object layer.
|
||||
setObjectLayer(newObject)
|
||||
|
||||
@@ -351,7 +354,7 @@ func initServer(ctx context.Context, newObject ObjectLayer) error {
|
||||
// All successful return.
|
||||
if globalIsDistErasure {
|
||||
// These messages only meant primarily for distributed setup, so only log during distributed setup.
|
||||
logger.Info("All MinIO sub-systems initialized successfully")
|
||||
logger.Info("All MinIO sub-systems initialized successfully in %s", time.Since(t1))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -453,6 +456,12 @@ func serverMain(ctx *cli.Context) {
|
||||
logger.Info(color.RedBold("WARNING: Detected Linux kernel version older than 4.0.0 release, there are some known potential performance problems with this kernel version. MinIO recommends a minimum of 4.x.x linux kernel version for best performance"))
|
||||
}
|
||||
|
||||
maxProcs := runtime.GOMAXPROCS(0)
|
||||
cpuProcs := runtime.NumCPU()
|
||||
if maxProcs < cpuProcs {
|
||||
logger.Info(color.RedBold("WARNING: Detected GOMAXPROCS(%d) < NumCPU(%d), please make sure to provide all PROCS to MinIO for optimal performance", maxProcs, cpuProcs))
|
||||
}
|
||||
|
||||
// Configure server.
|
||||
handler, err := configureServerHandler(globalEndpoints)
|
||||
if err != nil {
|
||||
|
||||
@@ -63,7 +63,6 @@ type StorageAPI interface {
|
||||
// returns 'nil' once healing is complete or if the disk
|
||||
// has never been replaced.
|
||||
Healing() *healingTracker
|
||||
|
||||
DiskInfo(ctx context.Context) (info DiskInfo, err error)
|
||||
NSScanner(ctx context.Context, cache dataUsageCache, updates chan<- dataUsageEntry) (dataUsageCache, error)
|
||||
|
||||
|
||||
Vendored
BIN
Binary file not shown.
@@ -148,6 +148,9 @@ func (config *TierConfigMgr) Verify(ctx context.Context, tier string) error {
|
||||
|
||||
// Empty returns if tier targets are empty
|
||||
func (config *TierConfigMgr) Empty() bool {
|
||||
if config == nil {
|
||||
return true
|
||||
}
|
||||
return len(config.ListTiers()) == 0
|
||||
}
|
||||
|
||||
|
||||
@@ -71,14 +71,11 @@ const (
|
||||
|
||||
// Detects change in underlying disk.
|
||||
type xlStorageDiskIDCheck struct {
|
||||
// fields position optimized for memory please
|
||||
// do not re-order them, if you add new fields
|
||||
// please use `fieldalignment ./...` to check
|
||||
// if your changes are not causing any problems.
|
||||
storage *xlStorage
|
||||
// apiCalls should be placed first so alignment is guaranteed for atomic operations.
|
||||
apiCalls [storageMetricLast]uint64
|
||||
apiLatencies [storageMetricLast]*lockedLastMinuteLatency
|
||||
diskID string
|
||||
apiCalls [storageMetricLast]uint64
|
||||
storage *xlStorage
|
||||
health *diskHealthTracker
|
||||
}
|
||||
|
||||
@@ -522,7 +519,7 @@ const (
|
||||
|
||||
// diskMaxConcurrent is the maximum number of running concurrent operations
|
||||
// for local and (incoming) remote disk ops respectively.
|
||||
var diskMaxConcurrent = 50
|
||||
var diskMaxConcurrent = 512
|
||||
|
||||
func init() {
|
||||
if s, ok := os.LookupEnv("_MINIO_DISK_MAX_CONCURRENT"); ok && s != "" {
|
||||
|
||||
@@ -595,6 +595,7 @@ func (j xlMetaV2Object) ToFileInfo(volume, path string) (FileInfo, error) {
|
||||
}
|
||||
}
|
||||
fi.ReplicationState = getInternalReplicationState(fi.Metadata)
|
||||
fi.Deleted = !fi.VersionPurgeStatus().Empty()
|
||||
replStatus := fi.ReplicationState.CompositeReplicationStatus()
|
||||
if replStatus != "" {
|
||||
fi.Metadata[xhttp.AmzBucketReplicationStatus] = string(replStatus)
|
||||
@@ -658,7 +659,7 @@ func readXLMetaNoData(r io.Reader, size int64) ([]byte, error) {
|
||||
buf := metaDataPoolGet()[:initial]
|
||||
_, err := io.ReadFull(r, buf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("readXLMetaNoData.ReadFull: %w", err)
|
||||
return nil, fmt.Errorf("readXLMetaNoData(io.ReadFull): %w", err)
|
||||
}
|
||||
readMore := func(n int64) error {
|
||||
has := int64(len(buf))
|
||||
@@ -679,9 +680,9 @@ func readXLMetaNoData(r io.Reader, size int64) ([]byte, error) {
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
// Returned if we read nothing.
|
||||
return fmt.Errorf("readXLMetaNoData.readMore: %w", io.ErrUnexpectedEOF)
|
||||
err = io.ErrUnexpectedEOF
|
||||
}
|
||||
return fmt.Errorf("readXLMetaNoData.readMore: %w", err)
|
||||
return fmt.Errorf("readXLMetaNoData(readMore): %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -699,7 +700,7 @@ func readXLMetaNoData(r io.Reader, size int64) ([]byte, error) {
|
||||
case 1, 2, 3:
|
||||
sz, tmp, err := msgp.ReadBytesHeader(tmp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("readXLMetaNoData(read_meta): uknown metadata version %w", err)
|
||||
}
|
||||
want := int64(sz) + int64(len(buf)-len(tmp))
|
||||
|
||||
@@ -720,10 +721,14 @@ func readXLMetaNoData(r io.Reader, size int64) ([]byte, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if int64(len(buf)) < want {
|
||||
return nil, fmt.Errorf("buffer shorter than expected (buflen: %d, want: %d): %w", len(buf), want, errFileCorrupt)
|
||||
}
|
||||
|
||||
tmp = buf[want:]
|
||||
_, after, err := msgp.ReadUint32Bytes(tmp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("readXLMetaNoData(read_meta): unknown metadata version %w", err)
|
||||
}
|
||||
want += int64(len(tmp) - len(after))
|
||||
|
||||
@@ -1277,7 +1282,7 @@ func (x *xlMetaV2) DeleteVersion(fi FileInfo) (string, error) {
|
||||
ver.ObjectV2.MetaSys[k] = []byte(v)
|
||||
}
|
||||
err = x.setIdx(i, *ver)
|
||||
return "", err
|
||||
return uuid.UUID(ver.ObjectV2.DataDir).String(), err
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1686,7 +1691,7 @@ func (x xlMetaV2) ListVersions(volume, path string) ([]FileInfo, error) {
|
||||
// Quorum must be the minimum number of matching metadata files.
|
||||
// Quorum should be > 1 and <= len(versions).
|
||||
// If strict is set to false, entries that match type
|
||||
func mergeXLV2Versions(quorum int, strict bool, versions ...[]xlMetaV2ShallowVersion) (merged []xlMetaV2ShallowVersion) {
|
||||
func mergeXLV2Versions(quorum int, strict bool, requestedVersions int, versions ...[]xlMetaV2ShallowVersion) (merged []xlMetaV2ShallowVersion) {
|
||||
if quorum <= 0 {
|
||||
quorum = 1
|
||||
}
|
||||
@@ -1703,6 +1708,8 @@ func mergeXLV2Versions(quorum int, strict bool, versions ...[]xlMetaV2ShallowVer
|
||||
// Shallow copy input
|
||||
versions = append(make([][]xlMetaV2ShallowVersion, 0, len(versions)), versions...)
|
||||
|
||||
var nVersions int // captures all non-free versions
|
||||
|
||||
// Our result
|
||||
merged = make([]xlMetaV2ShallowVersion, 0, len(versions[0]))
|
||||
tops := make([]xlMetaV2ShallowVersion, len(versions))
|
||||
@@ -1739,6 +1746,12 @@ func mergeXLV2Versions(quorum int, strict bool, versions ...[]xlMetaV2ShallowVer
|
||||
latest = tops[0]
|
||||
latestCount = len(tops)
|
||||
merged = append(merged, latest)
|
||||
|
||||
// Calculate latest 'n' non-free versions.
|
||||
if !latest.header.FreeVersion() {
|
||||
nVersions++
|
||||
}
|
||||
|
||||
} else {
|
||||
// Find latest.
|
||||
for i, ver := range tops {
|
||||
@@ -1802,6 +1815,11 @@ func mergeXLV2Versions(quorum int, strict bool, versions ...[]xlMetaV2ShallowVer
|
||||
}
|
||||
if latestCount >= quorum {
|
||||
merged = append(merged, latest)
|
||||
|
||||
// Calculate latest 'n' non-free versions.
|
||||
if !latest.header.FreeVersion() {
|
||||
nVersions++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1835,7 +1853,13 @@ func mergeXLV2Versions(quorum int, strict bool, versions ...[]xlMetaV2ShallowVer
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if requestedVersions > 0 && requestedVersions == nVersions {
|
||||
merged = append(merged, versions[0]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Sanity check. Enable if duplicates show up.
|
||||
if false {
|
||||
found := make(map[[16]byte]struct{})
|
||||
|
||||
@@ -18,10 +18,13 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sort"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -34,6 +37,29 @@ import (
|
||||
"github.com/minio/minio/internal/ioutil"
|
||||
)
|
||||
|
||||
func TestReadXLMetaNoData(t *testing.T) {
|
||||
f, err := os.Open("testdata/xl.meta-corrupt.gz")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
gz, err := gzip.NewReader(bufio.NewReader(f))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
buf, err := io.ReadAll(gz)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = readXLMetaNoData(bytes.NewReader(buf), int64(len(buf)))
|
||||
if err == nil {
|
||||
t.Fatal("expected error but returned success")
|
||||
}
|
||||
}
|
||||
|
||||
func TestXLV2FormatData(t *testing.T) {
|
||||
failOnErr := func(err error) {
|
||||
t.Helper()
|
||||
@@ -375,6 +401,55 @@ func TestDeleteVersionWithSharedDataDir(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_mergeXLV2Versions(b *testing.B) {
|
||||
data, err := ioutil.ReadFile("testdata/xl.meta-v1.2.zst")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
dec, _ := zstd.NewReader(nil)
|
||||
data, err = dec.DecodeAll(data, nil)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
var xl xlMetaV2
|
||||
if err = xl.LoadOrConvert(data); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
vers := make([][]xlMetaV2ShallowVersion, 16)
|
||||
for i := range vers {
|
||||
vers[i] = xl.versions
|
||||
}
|
||||
|
||||
b.Run("requested-none", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
b.SetBytes(855) // number of versions...
|
||||
for i := 0; i < b.N; i++ {
|
||||
mergeXLV2Versions(8, false, 0, vers...)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("requested-v1", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
b.SetBytes(855) // number of versions...
|
||||
for i := 0; i < b.N; i++ {
|
||||
mergeXLV2Versions(8, false, 1, vers...)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("requested-v2", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
b.SetBytes(855) // number of versions...
|
||||
for i := 0; i < b.N; i++ {
|
||||
mergeXLV2Versions(8, false, 1, vers...)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func Benchmark_xlMetaV2Shallow_Load(b *testing.B) {
|
||||
data, err := ioutil.ReadFile("testdata/xl.meta-v1.2.zst")
|
||||
if err != nil {
|
||||
@@ -398,6 +473,7 @@ func Benchmark_xlMetaV2Shallow_Load(b *testing.B) {
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("indexed", func(b *testing.B) {
|
||||
var xl xlMetaV2
|
||||
err = xl.Load(data)
|
||||
@@ -524,7 +600,7 @@ func Test_mergeXLV2Versions(t *testing.T) {
|
||||
|
||||
for i := range vers {
|
||||
t.Run(fmt.Sprintf("non-strict-q%d", i), func(t *testing.T) {
|
||||
merged := mergeXLV2Versions(i, false, vers...)
|
||||
merged := mergeXLV2Versions(i, false, 0, vers...)
|
||||
if len(merged) == 0 {
|
||||
t.Error("Did not get any results")
|
||||
return
|
||||
@@ -536,7 +612,7 @@ func Test_mergeXLV2Versions(t *testing.T) {
|
||||
}
|
||||
})
|
||||
t.Run(fmt.Sprintf("strict-q%d", i), func(t *testing.T) {
|
||||
merged := mergeXLV2Versions(i, true, vers...)
|
||||
merged := mergeXLV2Versions(i, true, 0, vers...)
|
||||
if len(merged) == 0 {
|
||||
t.Error("Did not get any results")
|
||||
return
|
||||
@@ -558,7 +634,7 @@ func Test_mergeXLV2Versions(t *testing.T) {
|
||||
}
|
||||
vMod = append(vMod, newVers)
|
||||
}
|
||||
merged := mergeXLV2Versions(i, false, vMod...)
|
||||
merged := mergeXLV2Versions(i, false, 0, vMod...)
|
||||
if len(merged) == 0 {
|
||||
t.Error("Did not get any results")
|
||||
return
|
||||
@@ -580,7 +656,7 @@ func Test_mergeXLV2Versions(t *testing.T) {
|
||||
}
|
||||
vMod = append(vMod, newVers)
|
||||
}
|
||||
merged := mergeXLV2Versions(i, false, vMod...)
|
||||
merged := mergeXLV2Versions(i, false, 0, vMod...)
|
||||
if len(merged) == 0 && i < 2 {
|
||||
t.Error("Did not get any results")
|
||||
return
|
||||
@@ -606,7 +682,7 @@ func Test_mergeXLV2Versions(t *testing.T) {
|
||||
}
|
||||
vMod = append(vMod, newVers)
|
||||
}
|
||||
merged := mergeXLV2Versions(i, false, vMod...)
|
||||
merged := mergeXLV2Versions(i, false, 0, vMod...)
|
||||
if len(merged) == 0 {
|
||||
t.Error("Did not get any results")
|
||||
return
|
||||
@@ -628,7 +704,7 @@ func Test_mergeXLV2Versions(t *testing.T) {
|
||||
}
|
||||
vMod = append(vMod, newVers)
|
||||
}
|
||||
merged := mergeXLV2Versions(i, false, vMod...)
|
||||
merged := mergeXLV2Versions(i, false, 0, vMod...)
|
||||
if len(merged) == 0 && i < 2 {
|
||||
t.Error("Did not get any results")
|
||||
return
|
||||
@@ -654,7 +730,7 @@ func Test_mergeXLV2Versions(t *testing.T) {
|
||||
}
|
||||
vMod = append(vMod, newVers)
|
||||
}
|
||||
merged := mergeXLV2Versions(i, true, vMod...)
|
||||
merged := mergeXLV2Versions(i, true, 0, vMod...)
|
||||
if len(merged) == 0 && i < 2 {
|
||||
t.Error("Did not get any results")
|
||||
return
|
||||
@@ -680,7 +756,7 @@ func Test_mergeXLV2Versions(t *testing.T) {
|
||||
}
|
||||
vMod = append(vMod, newVers)
|
||||
}
|
||||
merged := mergeXLV2Versions(i, true, vMod...)
|
||||
merged := mergeXLV2Versions(i, true, 0, vMod...)
|
||||
if len(merged) == 0 && i < 2 {
|
||||
t.Error("Did not get any results")
|
||||
return
|
||||
@@ -706,7 +782,7 @@ func Test_mergeXLV2Versions(t *testing.T) {
|
||||
}
|
||||
vMod = append(vMod, newVers)
|
||||
}
|
||||
merged := mergeXLV2Versions(i, true, vMod...)
|
||||
merged := mergeXLV2Versions(i, true, 0, vMod...)
|
||||
if len(merged) == 0 && i < 2 {
|
||||
t.Error("Did not get any results")
|
||||
return
|
||||
@@ -732,7 +808,7 @@ func Test_mergeXLV2Versions(t *testing.T) {
|
||||
}
|
||||
vMod = append(vMod, newVers)
|
||||
}
|
||||
merged := mergeXLV2Versions(i, true, vMod...)
|
||||
merged := mergeXLV2Versions(i, true, 0, vMod...)
|
||||
if len(merged) == 0 && i < 2 {
|
||||
t.Error("Did not get any results")
|
||||
return
|
||||
|
||||
@@ -398,6 +398,9 @@ func (s *xlStorage) readMetadataWithDMTime(ctx context.Context, itemPath string)
|
||||
}
|
||||
}
|
||||
buf, err := readXLMetaNoData(f, stat.Size())
|
||||
if err != nil {
|
||||
return nil, stat.ModTime().UTC(), fmt.Errorf("%w -> %s", err, itemPath)
|
||||
}
|
||||
return buf, stat.ModTime().UTC(), err
|
||||
}
|
||||
|
||||
|
||||
+6
-6
@@ -88,9 +88,9 @@ Date: Mon Nov 8 08:41:27 2021 -0800
|
||||
λ git cherry-pick 4f3317effea38c203c358af9cb5ce3c0e4173976
|
||||
```
|
||||
|
||||
*A self contained **patch** usually applies fine on the hotfix branch during backports as long it is self contained. There are situations however this may lead to conflicts and the patch will not cleanly apply. Conflicts might be trivial which can be resolved easily, when conflicts seem to be non-trivial or touches the part of the code-base the developer is not aware of - to get additional clarity reaching out of #hack on MinIOHQ slack channel is advised.*
|
||||
*A self contained **patch** usually applies fine on the hotfix branch during backports as long it is self contained. There are situations however this may lead to conflicts and the patch will not cleanly apply. Conflicts might be trivial which can be resolved easily, when conflicts seem to be non-trivial or touches the part of the code-base the developer is not confident - to get additional clarity reach out to #hack on MinIOHQ slack channel. Hasty changes must be avoided, minor fixes and logs may be added to hotfix branches but this should not be followed as practice.*
|
||||
|
||||
Once the **patch** is successfully applied, developer must run tests to alidate the fix that was backported by running following tests, locally.
|
||||
Once the **patch** is successfully applied, developer must run tests to validate the fix that was backported by running following tests, locally.
|
||||
|
||||
Unit tests
|
||||
|
||||
@@ -116,16 +116,16 @@ At this point in time the backport is ready to be submitted as a pull request to
|
||||
|
||||
To add a hotfix tag to the binary version and embed the relevant `commit-id` following build helpers are available
|
||||
|
||||
#### Builds the hotfix binary
|
||||
#### Builds the hotfix binary and uploads to https;//dl.min.io
|
||||
|
||||
```
|
||||
λ CRED_DIR=/media/builder/minio make hotfix
|
||||
λ CRED_DIR=/media/builder/minio make hotfix-push
|
||||
```
|
||||
|
||||
#### Builds the hotfix container
|
||||
#### Builds the hotfix container and pushes to docker.io/minio/minio
|
||||
|
||||
```
|
||||
λ CRED_DIR=/media/builder/minio make docker-hotfix
|
||||
λ CRED_DIR=/media/builder/minio make docker-hotfix-push
|
||||
```
|
||||
|
||||
Once this has been provided to the customer relevant binary will be uploaded from our *release server* securely, directly to <https://dl.minio.io/server/minio/hotfixes/archive/>
|
||||
|
||||
@@ -120,6 +120,20 @@ Encrypted :
|
||||
X-Amz-Server-Side-Encryption: AES256
|
||||
```
|
||||
|
||||
## Encrypted Private Key
|
||||
|
||||
MinIO supports encrypted KES client private keys. Therefore, you can use
|
||||
an password-protected private keys for `MINIO_KMS_KES_KEY_FILE`.
|
||||
|
||||
When using password-protected private keys for accessing KES you need to
|
||||
provide the password via:
|
||||
```
|
||||
export MINIO_KMS_KES_KEY_PASSWORD=<your-password>
|
||||
```
|
||||
|
||||
Note that MinIO only supports encrypted private keys - not encrypted certificates.
|
||||
Certificates are no secrets and sent in plaintext as part of the TLS handshake.
|
||||
|
||||
## Explore Further
|
||||
|
||||
- [Use `mc` with MinIO Server](https://docs.min.io/docs/minio-client-quickstart-guide)
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
# The address:port of the kes server - i.e. on the local machine.
|
||||
address = "127.0.0.1:7373"
|
||||
|
||||
[tls]
|
||||
key = "./kes-tls.key"
|
||||
cert = "./kes-tls.crt"
|
||||
|
||||
[policy.minio]
|
||||
paths = [
|
||||
"/v1/key/create/minio-*",
|
||||
"/v1/key/generate/minio-*",
|
||||
"/v1/key/decrypt/minio-*"
|
||||
]
|
||||
identities = [ "dd46485bedc9ad2909d2e8f9017216eec4413bc5c64b236d992f7ec19c843c5f" ]
|
||||
|
||||
[cache.expiry]
|
||||
all = "5m"
|
||||
unused = "20s"
|
||||
|
||||
[keystore.vault]
|
||||
address = "https://127.0.0.1:8200" # The Vault endpoint - i.e. https://127.0.0.1:8200
|
||||
name = "minio" # The domain resp. prefix at Vault's K/V backend
|
||||
|
||||
[keystore.vault.approle]
|
||||
id = "" # Your AppRole Role ID
|
||||
secret = "" # Your AppRole Secret ID
|
||||
retry = "15s" # Duration until the server tries to re-authenticate after connection loss.
|
||||
|
||||
[keystore.vault.tls]
|
||||
ca = "./vault-tls.crt" # Since we use self-signed certificates
|
||||
|
||||
[keystore.vault.status]
|
||||
ping = "10s"
|
||||
@@ -55,7 +55,7 @@ These metrics can be from any MinIO server once per collection.
|
||||
| `minio_s3_requests_error_total` | Total number S3 requests with errors |
|
||||
| `minio_s3_requests_inflight_total` | Total number of S3 requests currently in flight |
|
||||
| `minio_s3_requests_total` | Total number S3 requests |
|
||||
| `minio_s3_time_ttbf_seconds_distribution` | Distribution of the time to first byte across API calls. |
|
||||
| `minio_s3_time_ttfb_seconds_distribution` | Distribution of the time to first byte across API calls. |
|
||||
| `minio_s3_traffic_received_bytes` | Total number of s3 bytes received. |
|
||||
| `minio_s3_traffic_sent_bytes` | Total number of s3 bytes sent |
|
||||
| `minio_software_commit_info` | Git commit hash for the MinIO release. |
|
||||
|
||||
@@ -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.2022-03-08T22-28-51Z
|
||||
image: quay.io/minio/minio:RELEASE.2022-03-26T06-49-28Z
|
||||
command: server --console-address ":9001" http://minio{1...4}/data{1...2}
|
||||
expose:
|
||||
- "9000"
|
||||
|
||||
@@ -173,6 +173,17 @@ if [ $? -ne 0 ]; then
|
||||
exit_1;
|
||||
fi
|
||||
|
||||
err_minio2=$(./mc stat minio2/newbucket/xxx --json | jq -r .error.cause.message)
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "expecting object to be missing. exiting.."
|
||||
exit_1;
|
||||
fi
|
||||
|
||||
if [ "${err_minio2}" != "Object does not exist" ]; then
|
||||
echo "expected to see Object does not exist error, exiting..."
|
||||
exit_1;
|
||||
fi
|
||||
|
||||
./mc cp README.md minio2/newbucket/
|
||||
|
||||
sleep 5
|
||||
|
||||
+1
-1
@@ -110,7 +110,7 @@ export MINIO_IDENTITY_LDAP_SERVER_ADDR=myldapserver.com:636
|
||||
export MINIO_IDENTITY_LDAP_LOOKUP_BIND_DN='cn=admin,dc=min,dc=io'
|
||||
export MINIO_IDENTITY_LDAP_LOOKUP_BIND_PASSWORD=admin
|
||||
export MINIO_IDENTITY_LDAP_USER_DN_SEARCH_BASE_DN='ou=hwengg,dc=min,dc=io'
|
||||
export MINIO_IDENTITY_LDAP_USER_DN_SEARCH_FILTER='(uid=%s,cn=accounts,dc=min,dc=io)'
|
||||
export MINIO_IDENTITY_LDAP_USER_DN_SEARCH_FILTER='(uid=%s)'
|
||||
export MINIO_IDENTITY_LDAP_TLS_SKIP_VERIFY=on
|
||||
```
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module github.com/minio/minio
|
||||
|
||||
go 1.17
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
cloud.google.com/go/storage v1.10.0
|
||||
@@ -45,11 +45,11 @@ require (
|
||||
github.com/lib/pq v1.10.4
|
||||
github.com/miekg/dns v1.1.46
|
||||
github.com/minio/cli v1.22.0
|
||||
github.com/minio/console v0.15.2
|
||||
github.com/minio/console v0.15.8
|
||||
github.com/minio/csvparser v1.0.0
|
||||
github.com/minio/dperf v0.3.4
|
||||
github.com/minio/highwayhash v1.0.2
|
||||
github.com/minio/kes v0.18.0
|
||||
github.com/minio/kes v0.19.0
|
||||
github.com/minio/madmin-go v1.3.5
|
||||
github.com/minio/minio-go/v7 v7.0.23
|
||||
github.com/minio/parquet-go v1.1.0
|
||||
@@ -61,8 +61,8 @@ require (
|
||||
github.com/minio/zipindex v0.2.1
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
github.com/montanaflynn/stats v0.6.6
|
||||
github.com/nats-io/nats-server/v2 v2.7.2
|
||||
github.com/nats-io/nats.go v1.13.1-0.20220121202836-972a071d373d
|
||||
github.com/nats-io/nats-server/v2 v2.7.4
|
||||
github.com/nats-io/nats.go v1.13.1-0.20220308171302-2f2f6968e98d
|
||||
github.com/nats-io/stan.go v0.10.2
|
||||
github.com/ncw/directio v1.0.5
|
||||
github.com/nsqio/go-nsq v1.0.8
|
||||
@@ -95,136 +95,7 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.100.2 // indirect
|
||||
cloud.google.com/go/compute v0.1.0 // indirect
|
||||
cloud.google.com/go/iam v0.2.0 // indirect
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c // indirect
|
||||
github.com/PuerkitoBio/purell v1.1.1 // indirect
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||
github.com/apache/thrift v0.15.0 // indirect
|
||||
github.com/armon/go-metrics v0.3.3 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.2.0 // indirect
|
||||
github.com/charmbracelet/bubbles v0.10.3 // indirect
|
||||
github.com/charmbracelet/bubbletea v0.20.0 // indirect
|
||||
github.com/charmbracelet/lipgloss v0.5.0 // indirect
|
||||
github.com/containerd/console v1.0.3 // indirect
|
||||
github.com/coreos/go-semver v0.3.0 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
|
||||
github.com/docker/go-units v0.4.0 // indirect
|
||||
github.com/eapache/go-resiliency v1.2.0 // indirect
|
||||
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21 // indirect
|
||||
github.com/eapache/queue v1.1.0 // indirect
|
||||
github.com/fatih/structs v1.1.0 // indirect
|
||||
github.com/frankban/quicktest v1.14.0 // indirect
|
||||
github.com/gdamore/encoding v1.0.0 // indirect
|
||||
github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.1 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/go-openapi/analysis v0.21.2 // indirect
|
||||
github.com/go-openapi/errors v0.20.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||
github.com/go-openapi/jsonreference v0.19.6 // indirect
|
||||
github.com/go-openapi/runtime v0.23.1 // indirect
|
||||
github.com/go-openapi/spec v0.20.4 // indirect
|
||||
github.com/go-openapi/strfmt v0.21.2 // indirect
|
||||
github.com/go-openapi/swag v0.21.1 // indirect
|
||||
github.com/go-openapi/validate v0.21.0 // indirect
|
||||
github.com/go-stack/stack v1.8.1 // indirect
|
||||
github.com/goccy/go-json v0.9.4 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.1.1 // indirect
|
||||
github.com/gorilla/websocket v1.5.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-immutable-radix v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/go-uuid v1.0.2 // indirect
|
||||
github.com/jcmturner/aescts/v2 v2.0.0 // indirect
|
||||
github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect
|
||||
github.com/jcmturner/gofork v1.0.0 // indirect
|
||||
github.com/jcmturner/goidentity/v6 v6.0.1 // indirect
|
||||
github.com/jcmturner/rpc/v2 v2.0.3 // indirect
|
||||
github.com/jessevdk/go-flags v1.5.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect
|
||||
github.com/lestrrat-go/blackmagic v1.0.0 // indirect
|
||||
github.com/lestrrat-go/httpcc v1.0.0 // indirect
|
||||
github.com/lestrrat-go/iter v1.0.1 // indirect
|
||||
github.com/lestrrat-go/jwx v1.2.19 // indirect
|
||||
github.com/lestrrat-go/option v1.0.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||
github.com/mattn/go-ieproxy v0.0.1 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
|
||||
github.com/minio/argon2 v1.0.0 // indirect
|
||||
github.com/minio/colorjson v1.0.1 // indirect
|
||||
github.com/minio/filepath v1.0.0 // indirect
|
||||
github.com/minio/mc v0.0.0-20220302011226-f13defa54577 // indirect
|
||||
github.com/minio/md5-simd v1.1.2 // indirect
|
||||
github.com/mitchellh/mapstructure v1.4.3 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 // indirect
|
||||
github.com/muesli/reflow v0.3.0 // indirect
|
||||
github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 // indirect
|
||||
github.com/nats-io/jwt/v2 v2.2.1-0.20220113022732-58e87895b296 // indirect
|
||||
github.com/nats-io/nats-streaming-server v0.24.1 // indirect
|
||||
github.com/nats-io/nkeys v0.3.0 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
github.com/navidys/tvxwidgets v0.1.0 // indirect
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||
github.com/pkg/xattr v0.4.5 // indirect
|
||||
github.com/posener/complete v1.2.3 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c // indirect
|
||||
github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021 // indirect
|
||||
github.com/prometheus/common v0.32.1 // indirect
|
||||
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect
|
||||
github.com/rivo/tview v0.0.0-20220216162559-96063d6082f3 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/rjeczalik/notify v0.9.2 // indirect
|
||||
github.com/rogpeppe/go-internal v1.8.1 // indirect
|
||||
github.com/rs/xid v1.3.0 // indirect
|
||||
github.com/sirupsen/logrus v1.8.1 // indirect
|
||||
github.com/tidwall/gjson v1.14.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/tidwall/sjson v1.2.3 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.10 // indirect
|
||||
github.com/tklauser/numcpus v0.4.0 // indirect
|
||||
github.com/unrolled/secure v1.10.0 // indirect
|
||||
github.com/xdg/stringprep v1.0.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.2 // indirect
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.2 // indirect
|
||||
go.mongodb.org/mongo-driver v1.8.4 // indirect
|
||||
go.opencensus.io v0.23.0 // indirect
|
||||
go.uber.org/multierr v1.8.0 // indirect
|
||||
golang.org/x/mod v0.5.1 // indirect
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/tools v0.1.8 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20220302033224-9aa15565e42a // indirect
|
||||
google.golang.org/grpc v1.44.0 // indirect
|
||||
google.golang.org/protobuf v1.27.1 // indirect
|
||||
gopkg.in/h2non/filetype.v1 v1.0.5 // indirect
|
||||
gopkg.in/ini.v1 v1.66.4 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.5.1 // indirect
|
||||
maze.io/x/duration v0.0.0-20160924141736-faac084b6075 // indirect
|
||||
)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,8 +1,8 @@
|
||||
apiVersion: v1
|
||||
description: Multi-Cloud Object Storage
|
||||
name: minio
|
||||
version: 3.5.9
|
||||
appVersion: RELEASE.2022-03-03T21-21-16Z
|
||||
version: 3.6.3
|
||||
appVersion: RELEASE.2022-03-24T00-43-44Z
|
||||
keywords:
|
||||
- minio
|
||||
- storage
|
||||
|
||||
@@ -60,6 +60,9 @@ spec:
|
||||
runAsUser: {{ .Values.securityContext.runAsUser }}
|
||||
runAsGroup: {{ .Values.securityContext.runAsGroup }}
|
||||
fsGroup: {{ .Values.securityContext.fsGroup }}
|
||||
{{- if and (ge .Capabilities.KubeVersion.Major "1") (ge .Capabilities.KubeVersion.Minor "20") }}
|
||||
fsGroupChangePolicy: {{ .Values.securityContext.fsGroupChangePolicy }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{ if .Values.serviceAccount.create }}
|
||||
serviceAccountName: {{ .Values.serviceAccount.name }}
|
||||
|
||||
@@ -17,6 +17,7 @@ spec:
|
||||
ingress:
|
||||
- ports:
|
||||
- port: {{ .Values.service.port }}
|
||||
- port: {{ .Values.consoleService.port }}
|
||||
{{- if not .Values.networkPolicy.allowExternal }}
|
||||
from:
|
||||
- podSelector:
|
||||
|
||||
@@ -87,6 +87,9 @@ spec:
|
||||
runAsUser: {{ .Values.securityContext.runAsUser }}
|
||||
runAsGroup: {{ .Values.securityContext.runAsGroup }}
|
||||
fsGroup: {{ .Values.securityContext.fsGroup }}
|
||||
{{- if and (ge .Capabilities.KubeVersion.Major "1") (ge .Capabilities.KubeVersion.Minor "20") }}
|
||||
fsGroupChangePolicy: {{ .Values.securityContext.fsGroupChangePolicy }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{ if .Values.serviceAccount.create }}
|
||||
serviceAccountName: {{ .Values.serviceAccount.name }}
|
||||
|
||||
@@ -14,7 +14,7 @@ clusterDomain: cluster.local
|
||||
##
|
||||
image:
|
||||
repository: quay.io/minio/minio
|
||||
tag: RELEASE.2021-12-29T06-49-06Z
|
||||
tag: RELEASE.2022-03-24T00-43-44Z
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
imagePullSecrets: []
|
||||
@@ -25,7 +25,7 @@ imagePullSecrets: []
|
||||
##
|
||||
mcImage:
|
||||
repository: quay.io/minio/mc
|
||||
tag: RELEASE.2021-12-29T06-52-55Z
|
||||
tag: RELEASE.2022-03-17T20-25-06Z
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
## minio mode, i.e. standalone or distributed or gateway.
|
||||
@@ -237,6 +237,7 @@ securityContext:
|
||||
runAsUser: 1000
|
||||
runAsGroup: 1000
|
||||
fsGroup: 1000
|
||||
fsGroupChangePolicy: "OnRootMismatch"
|
||||
|
||||
# Additational pod annotations
|
||||
podAnnotations: {}
|
||||
|
||||
+135
-47
@@ -1,9 +1,97 @@
|
||||
apiVersion: v1
|
||||
entries:
|
||||
minio:
|
||||
- apiVersion: v1
|
||||
appVersion: RELEASE.2022-03-24T00-43-44Z
|
||||
created: "2022-03-23T21:06:44.793706115-07:00"
|
||||
description: Multi-Cloud Object Storage
|
||||
digest: 99508b20eb0083a567dcccaf9a6c237e09575ed1d70cd2e8333f89c472d13d75
|
||||
home: https://min.io
|
||||
icon: https://min.io/resources/img/logo/MINIO_wordmark.png
|
||||
keywords:
|
||||
- minio
|
||||
- storage
|
||||
- object-storage
|
||||
- s3
|
||||
- cluster
|
||||
maintainers:
|
||||
- email: dev@minio.io
|
||||
name: MinIO, Inc
|
||||
name: minio
|
||||
sources:
|
||||
- https://github.com/minio/minio
|
||||
urls:
|
||||
- https://charts.min.io/helm-releases/minio-3.6.3.tgz
|
||||
version: 3.6.3
|
||||
- apiVersion: v1
|
||||
appVersion: RELEASE.2022-03-17T06-34-49Z
|
||||
created: "2022-03-23T21:06:44.792478365-07:00"
|
||||
description: Multi-Cloud Object Storage
|
||||
digest: b4cd25611ca322b1d23d23112fdfa6b068fd91eefe0b0663b88ff87ea4282495
|
||||
home: https://min.io
|
||||
icon: https://min.io/resources/img/logo/MINIO_wordmark.png
|
||||
keywords:
|
||||
- minio
|
||||
- storage
|
||||
- object-storage
|
||||
- s3
|
||||
- cluster
|
||||
maintainers:
|
||||
- email: dev@minio.io
|
||||
name: MinIO, Inc
|
||||
name: minio
|
||||
sources:
|
||||
- https://github.com/minio/minio
|
||||
urls:
|
||||
- https://charts.min.io/helm-releases/minio-3.6.2.tgz
|
||||
version: 3.6.2
|
||||
- apiVersion: v1
|
||||
appVersion: RELEASE.2022-03-14T18-25-24Z
|
||||
created: "2022-03-23T21:06:44.790639016-07:00"
|
||||
description: Multi-Cloud Object Storage
|
||||
digest: d75b88162bfe54740a233bcecf87328bba2ae23d170bec3a35c828bc6fdc224c
|
||||
home: https://min.io
|
||||
icon: https://min.io/resources/img/logo/MINIO_wordmark.png
|
||||
keywords:
|
||||
- minio
|
||||
- storage
|
||||
- object-storage
|
||||
- s3
|
||||
- cluster
|
||||
maintainers:
|
||||
- email: dev@minio.io
|
||||
name: MinIO, Inc
|
||||
name: minio
|
||||
sources:
|
||||
- https://github.com/minio/minio
|
||||
urls:
|
||||
- https://charts.min.io/helm-releases/minio-3.6.1.tgz
|
||||
version: 3.6.1
|
||||
- apiVersion: v1
|
||||
appVersion: RELEASE.2022-03-11T23-57-45Z
|
||||
created: "2022-03-23T21:06:44.789351564-07:00"
|
||||
description: Multi-Cloud Object Storage
|
||||
digest: 22e53a1184a21a679bc7d8b94e955777f3506340fc29da5ab0cb6d729bdbde8d
|
||||
home: https://min.io
|
||||
icon: https://min.io/resources/img/logo/MINIO_wordmark.png
|
||||
keywords:
|
||||
- minio
|
||||
- storage
|
||||
- object-storage
|
||||
- s3
|
||||
- cluster
|
||||
maintainers:
|
||||
- email: dev@minio.io
|
||||
name: MinIO, Inc
|
||||
name: minio
|
||||
sources:
|
||||
- https://github.com/minio/minio
|
||||
urls:
|
||||
- https://charts.min.io/helm-releases/minio-3.6.0.tgz
|
||||
version: 3.6.0
|
||||
- apiVersion: v1
|
||||
appVersion: RELEASE.2022-03-03T21-21-16Z
|
||||
created: "2022-03-03T15:28:46.71872892-08:00"
|
||||
created: "2022-03-23T21:06:44.788117819-07:00"
|
||||
description: Multi-Cloud Object Storage
|
||||
digest: 6fda968d3fdfd60470c0055a4e1a3bd8e5aee9ad0af5ba2fb7b7b926fdc9e4a0
|
||||
home: https://min.io
|
||||
@@ -25,7 +113,7 @@ entries:
|
||||
version: 3.5.9
|
||||
- apiVersion: v1
|
||||
appVersion: RELEASE.2022-02-26T02-54-46Z
|
||||
created: "2022-03-03T15:28:46.717568339-08:00"
|
||||
created: "2022-03-23T21:06:44.786549387-07:00"
|
||||
description: Multi-Cloud Object Storage
|
||||
digest: 8e015369048a3a82bbd53ad36696786f18561c6b25d14eee9e2c93a7336cef46
|
||||
home: https://min.io
|
||||
@@ -47,7 +135,7 @@ entries:
|
||||
version: 3.5.8
|
||||
- apiVersion: v1
|
||||
appVersion: RELEASE.2022-02-18T01-50-10Z
|
||||
created: "2022-03-03T15:28:46.716258212-08:00"
|
||||
created: "2022-03-23T21:06:44.785265725-07:00"
|
||||
description: Multi-Cloud Object Storage
|
||||
digest: cb3543fe748e5f0d59b3ccf4ab9af8e10b731405ae445d1f5715e30013632373
|
||||
home: https://min.io
|
||||
@@ -69,7 +157,7 @@ entries:
|
||||
version: 3.5.7
|
||||
- apiVersion: v1
|
||||
appVersion: RELEASE.2022-02-18T01-50-10Z
|
||||
created: "2022-03-03T15:28:46.714897775-08:00"
|
||||
created: "2022-03-23T21:06:44.783518199-07:00"
|
||||
description: Multi-Cloud Object Storage
|
||||
digest: f2e359fa5eefffc59abb3d14a8fa94b11ddeaa99f6cd8dd5f40f4e04121000d6
|
||||
home: https://min.io
|
||||
@@ -91,7 +179,7 @@ entries:
|
||||
version: 3.5.6
|
||||
- apiVersion: v1
|
||||
appVersion: RELEASE.2022-02-16T00-35-27Z
|
||||
created: "2022-03-03T15:28:46.713557317-08:00"
|
||||
created: "2022-03-23T21:06:44.782211855-07:00"
|
||||
description: Multi-Cloud Object Storage
|
||||
digest: 529d56cca9d83a3d0e5672e63b6e87b5bcbe10a6b45f7a55ba998cceb32f9c81
|
||||
home: https://min.io
|
||||
@@ -113,7 +201,7 @@ entries:
|
||||
version: 3.5.5
|
||||
- apiVersion: v1
|
||||
appVersion: RELEASE.2022-02-12T00-51-25Z
|
||||
created: "2022-03-03T15:28:46.711659592-08:00"
|
||||
created: "2022-03-23T21:06:44.780903215-07:00"
|
||||
description: Multi-Cloud Object Storage
|
||||
digest: 3d530598f8ece67bec5b7f990d206584893987c713502f9228e4ee24b5535414
|
||||
home: https://min.io
|
||||
@@ -135,7 +223,7 @@ entries:
|
||||
version: 3.5.4
|
||||
- apiVersion: v1
|
||||
appVersion: RELEASE.2022-02-12T00-51-25Z
|
||||
created: "2022-03-03T15:28:46.710394206-08:00"
|
||||
created: "2022-03-23T21:06:44.779556381-07:00"
|
||||
description: Multi-Cloud Object Storage
|
||||
digest: 53937031348b29615f07fc4869b2d668391d8ba9084630a497abd7a7dea9dfb0
|
||||
home: https://min.io
|
||||
@@ -157,7 +245,7 @@ entries:
|
||||
version: 3.5.3
|
||||
- apiVersion: v1
|
||||
appVersion: RELEASE.2022-02-07T08-17-33Z
|
||||
created: "2022-03-03T15:28:46.70926804-08:00"
|
||||
created: "2022-03-23T21:06:44.778364019-07:00"
|
||||
description: Multi-Cloud Object Storage
|
||||
digest: 68d643414ff0d565716c5715034fcbf1af262e041915a5c02eb51ec1a65c1ea0
|
||||
home: https://min.io
|
||||
@@ -179,7 +267,7 @@ entries:
|
||||
version: 3.5.2
|
||||
- apiVersion: v1
|
||||
appVersion: RELEASE.2022-02-01T18-00-14Z
|
||||
created: "2022-03-03T15:28:46.708131603-08:00"
|
||||
created: "2022-03-23T21:06:44.777066403-07:00"
|
||||
description: Multi-Cloud Object Storage
|
||||
digest: a3e855ed0f31233b989fffd775a29d6fbfa0590089010ff16783fd7f142ef6e7
|
||||
home: https://min.io
|
||||
@@ -201,7 +289,7 @@ entries:
|
||||
version: 3.5.1
|
||||
- apiVersion: v1
|
||||
appVersion: RELEASE.2022-02-01T18-00-14Z
|
||||
created: "2022-03-03T15:28:46.70701956-08:00"
|
||||
created: "2022-03-23T21:06:44.77532758-07:00"
|
||||
description: Multi-Cloud Object Storage
|
||||
digest: b1b0ae3c54b4260a698753e11d7781bb8ddc67b7e3fbf0af82796e4cd4ef92a3
|
||||
home: https://min.io
|
||||
@@ -223,7 +311,7 @@ entries:
|
||||
version: 3.5.0
|
||||
- apiVersion: v1
|
||||
appVersion: RELEASE.2022-01-28T02-28-16Z
|
||||
created: "2022-03-03T15:28:46.705883001-08:00"
|
||||
created: "2022-03-23T21:06:44.77418347-07:00"
|
||||
description: Multi-Cloud Object Storage
|
||||
digest: fecf25d2d3fb208c6f894fed642a60780a570b7f6d0adddde846af7236dc80aa
|
||||
home: https://min.io
|
||||
@@ -245,7 +333,7 @@ entries:
|
||||
version: 3.4.8
|
||||
- apiVersion: v1
|
||||
appVersion: RELEASE.2022-01-25T19-56-04Z
|
||||
created: "2022-03-03T15:28:46.704205655-08:00"
|
||||
created: "2022-03-23T21:06:44.773013848-07:00"
|
||||
description: Multi-Cloud Object Storage
|
||||
digest: c78008caa5ce98f64c887630f59d0cbd481cb3f19a7d4e9d3e81bf4e1e45cadc
|
||||
home: https://min.io
|
||||
@@ -267,7 +355,7 @@ entries:
|
||||
version: 3.4.7
|
||||
- apiVersion: v1
|
||||
appVersion: RELEASE.2022-01-08T03-11-54Z
|
||||
created: "2022-03-03T15:28:46.702952316-08:00"
|
||||
created: "2022-03-23T21:06:44.771918434-07:00"
|
||||
description: Multi-Cloud Object Storage
|
||||
digest: 8f2e2691bf897f74ff094dd370ec56ba9d417e5e8926710c14c2ba346330238d
|
||||
home: https://min.io
|
||||
@@ -289,7 +377,7 @@ entries:
|
||||
version: 3.4.6
|
||||
- apiVersion: v1
|
||||
appVersion: RELEASE.2022-01-04T07-41-07Z
|
||||
created: "2022-03-03T15:28:46.701754571-08:00"
|
||||
created: "2022-03-23T21:06:44.770733259-07:00"
|
||||
description: Multi-Cloud Object Storage
|
||||
digest: bacd140f0016fab35f516bde787da6449b3a960c071fad9e4b6563118033ac84
|
||||
home: https://min.io
|
||||
@@ -311,7 +399,7 @@ entries:
|
||||
version: 3.4.5
|
||||
- apiVersion: v1
|
||||
appVersion: RELEASE.2021-12-29T06-49-06Z
|
||||
created: "2022-03-03T15:28:46.700632058-08:00"
|
||||
created: "2022-03-23T21:06:44.769538258-07:00"
|
||||
description: Multi-Cloud Object Storage
|
||||
digest: 48a453ea5ffeef25933904caefd9470bfb26224dfc2d1096bd0031467ba53007
|
||||
home: https://min.io
|
||||
@@ -333,7 +421,7 @@ entries:
|
||||
version: 3.4.4
|
||||
- apiVersion: v1
|
||||
appVersion: RELEASE.2021-12-20T22-07-16Z
|
||||
created: "2022-03-03T15:28:46.699429459-08:00"
|
||||
created: "2022-03-23T21:06:44.7676082-07:00"
|
||||
description: Multi-Cloud Object Storage
|
||||
digest: 47ef4a930713b98f9438ceca913c6e700f85bb25dba5624b056486254b5f0c60
|
||||
home: https://min.io
|
||||
@@ -355,7 +443,7 @@ entries:
|
||||
version: 3.4.3
|
||||
- apiVersion: v1
|
||||
appVersion: RELEASE.2021-12-20T22-07-16Z
|
||||
created: "2022-03-03T15:28:46.698226992-08:00"
|
||||
created: "2022-03-23T21:06:44.766512145-07:00"
|
||||
description: Multi-Cloud Object Storage
|
||||
digest: d6763f7e2ea66810bd55eb225579a9c3b968f9ae1256f45fd469362e55d846ff
|
||||
home: https://min.io
|
||||
@@ -377,7 +465,7 @@ entries:
|
||||
version: 3.4.2
|
||||
- apiVersion: v1
|
||||
appVersion: RELEASE.2021-12-10T23-03-39Z
|
||||
created: "2022-03-03T15:28:46.696842536-08:00"
|
||||
created: "2022-03-23T21:06:44.765594753-07:00"
|
||||
description: Multi-Cloud Object Storage
|
||||
digest: 2fb822c87216ba3fc2ae51a54a0a3e239aa560d86542991504a841cc2a2b9a37
|
||||
home: https://min.io
|
||||
@@ -399,7 +487,7 @@ entries:
|
||||
version: 3.4.1
|
||||
- apiVersion: v1
|
||||
appVersion: RELEASE.2021-12-18T04-42-33Z
|
||||
created: "2022-03-03T15:28:46.695365796-08:00"
|
||||
created: "2022-03-23T21:06:44.76437481-07:00"
|
||||
description: Multi-Cloud Object Storage
|
||||
digest: fa8ba1aeb1a15316c6be8403416a5e6b5e6139b7166592087e7bddc9e6db5453
|
||||
home: https://min.io
|
||||
@@ -421,7 +509,7 @@ entries:
|
||||
version: 3.4.0
|
||||
- apiVersion: v1
|
||||
appVersion: RELEASE.2021-12-10T23-03-39Z
|
||||
created: "2022-03-03T15:28:46.694266323-08:00"
|
||||
created: "2022-03-23T21:06:44.763226393-07:00"
|
||||
description: Multi-Cloud Object Storage
|
||||
digest: b9b0af9ca50b8d00868e1f1b989dca275829d9110af6de91bb9b3a398341e894
|
||||
home: https://min.io
|
||||
@@ -443,7 +531,7 @@ entries:
|
||||
version: 3.3.4
|
||||
- apiVersion: v1
|
||||
appVersion: RELEASE.2021-12-10T23-03-39Z
|
||||
created: "2022-03-03T15:28:46.693191851-08:00"
|
||||
created: "2022-03-23T21:06:44.762111446-07:00"
|
||||
description: Multi-Cloud Object Storage
|
||||
digest: f8b22a5b8fe95a7ddf61b825e17d11c9345fb10e4c126b0d78381608aa300a08
|
||||
home: https://min.io
|
||||
@@ -465,7 +553,7 @@ entries:
|
||||
version: 3.3.3
|
||||
- apiVersion: v1
|
||||
appVersion: RELEASE.2021-12-10T23-03-39Z
|
||||
created: "2022-03-03T15:28:46.692145802-08:00"
|
||||
created: "2022-03-23T21:06:44.760903355-07:00"
|
||||
description: Multi-Cloud Object Storage
|
||||
digest: c48d474f269427abe5ab446f00687d0625b3d1adfc5c73bdb4b21ca9e42853fb
|
||||
home: https://min.io
|
||||
@@ -487,7 +575,7 @@ entries:
|
||||
version: 3.3.2
|
||||
- apiVersion: v1
|
||||
appVersion: RELEASE.2021-11-24T23-19-33Z
|
||||
created: "2022-03-03T15:28:46.690906843-08:00"
|
||||
created: "2022-03-23T21:06:44.759169595-07:00"
|
||||
description: Multi-Cloud Object Storage
|
||||
digest: 7c3da39d9b0090cbf5efedf0cc163a1e2df05becc5152c3add8e837384690bc4
|
||||
home: https://min.io
|
||||
@@ -509,7 +597,7 @@ entries:
|
||||
version: 3.3.1
|
||||
- apiVersion: v1
|
||||
appVersion: RELEASE.2021-11-24T23-19-33Z
|
||||
created: "2022-03-03T15:28:46.689306119-08:00"
|
||||
created: "2022-03-23T21:06:44.757570962-07:00"
|
||||
description: Multi-Cloud Object Storage
|
||||
digest: 50d6590b4cc779c40f81cc13b1586fbe508aa7f3230036c760bfc5f4154fbce4
|
||||
home: https://min.io
|
||||
@@ -531,7 +619,7 @@ entries:
|
||||
version: 3.3.0
|
||||
- apiVersion: v1
|
||||
appVersion: RELEASE.2021-10-13T00-23-17Z
|
||||
created: "2022-03-03T15:28:46.688201137-08:00"
|
||||
created: "2022-03-23T21:06:44.756372619-07:00"
|
||||
description: Multi-Cloud Object Storage
|
||||
digest: 5b797b7208cd904c11a76cd72938c8652160cb5fcd7f09fa41e4e703e6d64054
|
||||
home: https://min.io
|
||||
@@ -553,7 +641,7 @@ entries:
|
||||
version: 3.2.0
|
||||
- apiVersion: v1
|
||||
appVersion: RELEASE.2021-10-10T16-53-30Z
|
||||
created: "2022-03-03T15:28:46.686861384-08:00"
|
||||
created: "2022-03-23T21:06:44.755234538-07:00"
|
||||
description: Multi-Cloud Object Storage
|
||||
digest: e084ac4bb095f071e59f8f08bd092e4ab2404c1ddadacfdce7dbe248f1bafff8
|
||||
home: https://min.io
|
||||
@@ -575,7 +663,7 @@ entries:
|
||||
version: 3.1.9
|
||||
- apiVersion: v1
|
||||
appVersion: RELEASE.2021-10-06T23-36-31Z
|
||||
created: "2022-03-03T15:28:46.685072158-08:00"
|
||||
created: "2022-03-23T21:06:44.754118063-07:00"
|
||||
description: Multi-Cloud Object Storage
|
||||
digest: 2890430a8d9487d1fa5508c26776e4881d0086b2c052aa6bdc65c0e4423b9159
|
||||
home: https://min.io
|
||||
@@ -597,7 +685,7 @@ entries:
|
||||
version: 3.1.8
|
||||
- apiVersion: v1
|
||||
appVersion: RELEASE.2021-10-02T16-31-05Z
|
||||
created: "2022-03-03T15:28:46.683398819-08:00"
|
||||
created: "2022-03-23T21:06:44.752913852-07:00"
|
||||
description: Multi-Cloud Object Storage
|
||||
digest: 01a92196af6c47e3a01e1c68d7cf693a8bc487cba810c2cecff155071e4d6a11
|
||||
home: https://min.io
|
||||
@@ -619,7 +707,7 @@ entries:
|
||||
version: 3.1.7
|
||||
- apiVersion: v1
|
||||
appVersion: RELEASE.2021-09-18T18-09-59Z
|
||||
created: "2022-03-03T15:28:46.681758267-08:00"
|
||||
created: "2022-03-23T21:06:44.751805247-07:00"
|
||||
description: Multi-Cloud Object Storage
|
||||
digest: e779d73f80b75f33b9c9d995ab10fa455c9c57ee575ebc54e06725a64cd04310
|
||||
home: https://min.io
|
||||
@@ -641,7 +729,7 @@ entries:
|
||||
version: 3.1.6
|
||||
- apiVersion: v1
|
||||
appVersion: RELEASE.2021-09-18T18-09-59Z
|
||||
created: "2022-03-03T15:28:46.680475021-08:00"
|
||||
created: "2022-03-23T21:06:44.750080597-07:00"
|
||||
description: Multi-Cloud Object Storage
|
||||
digest: 19de4bbc8a400f0c2a94c5e85fc25c9bfc666e773fb3e368dd621d5a57dd1c2a
|
||||
home: https://min.io
|
||||
@@ -663,7 +751,7 @@ entries:
|
||||
version: 3.1.5
|
||||
- apiVersion: v1
|
||||
appVersion: RELEASE.2021-09-18T18-09-59Z
|
||||
created: "2022-03-03T15:28:46.678759552-08:00"
|
||||
created: "2022-03-23T21:06:44.749074579-07:00"
|
||||
description: Multi-Cloud Object Storage
|
||||
digest: f789d93a171296dd01af0105a5ce067c663597afbb2432faeda293b752b355c0
|
||||
home: https://min.io
|
||||
@@ -685,7 +773,7 @@ entries:
|
||||
version: 3.1.4
|
||||
- apiVersion: v1
|
||||
appVersion: RELEASE.2021-09-09T21-37-07Z
|
||||
created: "2022-03-03T15:28:46.677658732-08:00"
|
||||
created: "2022-03-23T21:06:44.748000876-07:00"
|
||||
description: Multi-Cloud Object Storage
|
||||
digest: e2eb34d31560b012ef6581f0ff6004ea4376c968cbe0daed2d8f3a614a892afb
|
||||
home: https://min.io
|
||||
@@ -707,7 +795,7 @@ entries:
|
||||
version: 3.1.3
|
||||
- apiVersion: v1
|
||||
appVersion: RELEASE.2021-09-09T21-37-07Z
|
||||
created: "2022-03-03T15:28:46.676660879-08:00"
|
||||
created: "2022-03-23T21:06:44.746861195-07:00"
|
||||
description: Multi-Cloud Object Storage
|
||||
digest: 8d7e0cc46b3583abd71b97dc0c071f98321101f90eca17348f1e9e0831be64cd
|
||||
home: https://min.io
|
||||
@@ -729,7 +817,7 @@ entries:
|
||||
version: 3.1.2
|
||||
- apiVersion: v1
|
||||
appVersion: RELEASE.2021-09-09T21-37-07Z
|
||||
created: "2022-03-03T15:28:46.675832123-08:00"
|
||||
created: "2022-03-23T21:06:44.745725291-07:00"
|
||||
description: Multi-Cloud Object Storage
|
||||
digest: 50dcbf366b1b21f4a6fc429d0b884c0c7ff481d0fb95c5e9b3ae157c348dd124
|
||||
home: https://min.io
|
||||
@@ -751,7 +839,7 @@ entries:
|
||||
version: 3.1.1
|
||||
- apiVersion: v1
|
||||
appVersion: RELEASE.2021-09-09T21-37-07Z
|
||||
created: "2022-03-03T15:28:46.674997142-08:00"
|
||||
created: "2022-03-23T21:06:44.744677475-07:00"
|
||||
description: Multi-Cloud Object Storage
|
||||
digest: 6c01af55d2e2e5f716eabf6fef3a92a8464d0674529e9bacab292e5478a73b7a
|
||||
home: https://min.io
|
||||
@@ -773,7 +861,7 @@ entries:
|
||||
version: 3.1.0
|
||||
- apiVersion: v1
|
||||
appVersion: RELEASE.2021-09-03T03-56-13Z
|
||||
created: "2022-03-03T15:28:46.673959757-08:00"
|
||||
created: "2022-03-23T21:06:44.743254312-07:00"
|
||||
description: Multi-Cloud Object Storage
|
||||
digest: 18e10be4d0458bc590ca9abf753227e0c70f60511495387b8d4fb15a4daf932e
|
||||
home: https://min.io
|
||||
@@ -795,7 +883,7 @@ entries:
|
||||
version: 3.0.2
|
||||
- apiVersion: v1
|
||||
appVersion: RELEASE.2021-08-31T05-46-54Z
|
||||
created: "2022-03-03T15:28:46.672774395-08:00"
|
||||
created: "2022-03-23T21:06:44.74197572-07:00"
|
||||
description: Multi-Cloud Object Storage
|
||||
digest: f5b6e7f6272a9e71aef3b75555f6f756a39eef65cb78873f26451dba79b19906
|
||||
home: https://min.io
|
||||
@@ -817,7 +905,7 @@ entries:
|
||||
version: 3.0.1
|
||||
- apiVersion: v1
|
||||
appVersion: RELEASE.2021-08-31T05-46-54Z
|
||||
created: "2022-03-03T15:28:46.671624084-08:00"
|
||||
created: "2022-03-23T21:06:44.738596434-07:00"
|
||||
description: Multi-Cloud Object Storage
|
||||
digest: 6d2ee1336c412affaaf209fdb80215be2a6ebb23ab2443adbaffef9e7df13fab
|
||||
home: https://min.io
|
||||
@@ -839,7 +927,7 @@ entries:
|
||||
version: 3.0.0
|
||||
- apiVersion: v1
|
||||
appVersion: RELEASE.2021-08-31T05-46-54Z
|
||||
created: "2022-03-03T15:28:46.670794348-08:00"
|
||||
created: "2022-03-23T21:06:44.737408609-07:00"
|
||||
description: Multi-Cloud Object Storage
|
||||
digest: 0a004aaf5bb61deed6a5c88256d1695ebe2f9ff1553874a93e4acfd75e8d339b
|
||||
home: https://min.io
|
||||
@@ -859,7 +947,7 @@ entries:
|
||||
version: 2.0.1
|
||||
- apiVersion: v1
|
||||
appVersion: RELEASE.2021-08-25T00-41-18Z
|
||||
created: "2022-03-03T15:28:46.669829694-08:00"
|
||||
created: "2022-03-23T21:06:44.736406913-07:00"
|
||||
description: Multi-Cloud Object Storage
|
||||
digest: fcd944e837ee481307de6aa3d387ea18c234f995a84c15abb211aab4a4054afc
|
||||
home: https://min.io
|
||||
@@ -879,7 +967,7 @@ entries:
|
||||
version: 2.0.0
|
||||
- apiVersion: v1
|
||||
appVersion: RELEASE.2021-08-25T00-41-18Z
|
||||
created: "2022-03-03T15:28:46.668829946-08:00"
|
||||
created: "2022-03-23T21:06:44.735376678-07:00"
|
||||
description: Multi-Cloud Object Storage
|
||||
digest: 7b6c033d43a856479eb493ab8ca05b230f77c3e42e209e8f298fac6af1a9796f
|
||||
home: https://min.io
|
||||
@@ -899,7 +987,7 @@ entries:
|
||||
version: 1.0.5
|
||||
- apiVersion: v1
|
||||
appVersion: RELEASE.2021-08-25T00-41-18Z
|
||||
created: "2022-03-03T15:28:46.667869747-08:00"
|
||||
created: "2022-03-23T21:06:44.734196178-07:00"
|
||||
description: Multi-Cloud Object Storage
|
||||
digest: abd221245ace16c8e0c6c851cf262d1474a5219dcbf25c4b2e7b77142f9c59ed
|
||||
home: https://min.io
|
||||
@@ -919,7 +1007,7 @@ entries:
|
||||
version: 1.0.4
|
||||
- apiVersion: v1
|
||||
appVersion: RELEASE.2021-08-20T18-32-01Z
|
||||
created: "2022-03-03T15:28:46.666658153-08:00"
|
||||
created: "2022-03-23T21:06:44.732657511-07:00"
|
||||
description: Multi-Cloud Object Storage
|
||||
digest: 922a333f5413d1042f7aa81929f43767f6ffca9b260c46713f04ce1dda86d57d
|
||||
home: https://min.io
|
||||
@@ -939,7 +1027,7 @@ entries:
|
||||
version: 1.0.3
|
||||
- apiVersion: v1
|
||||
appVersion: RELEASE.2021-08-20T18-32-01Z
|
||||
created: "2022-03-03T15:28:46.662395153-08:00"
|
||||
created: "2022-03-23T21:06:44.731547254-07:00"
|
||||
description: High Performance, Kubernetes Native Object Storage
|
||||
digest: 10e22773506bbfb1c66442937956534cf4057b94f06a977db78b8cd223588388
|
||||
home: https://min.io
|
||||
@@ -959,7 +1047,7 @@ entries:
|
||||
version: 1.0.2
|
||||
- apiVersion: v1
|
||||
appVersion: RELEASE.2021-08-20T18-32-01Z
|
||||
created: "2022-03-03T15:28:46.661418704-08:00"
|
||||
created: "2022-03-23T21:06:44.730531176-07:00"
|
||||
description: High Performance, Kubernetes Native Object Storage
|
||||
digest: ef86ab6df23d6942705da9ef70991b649638c51bc310587d37a425268ba4a06c
|
||||
home: https://min.io
|
||||
@@ -979,7 +1067,7 @@ entries:
|
||||
version: 1.0.1
|
||||
- apiVersion: v1
|
||||
appVersion: RELEASE.2021-08-17T20-53-08Z
|
||||
created: "2022-03-03T15:28:46.660257537-08:00"
|
||||
created: "2022-03-23T21:06:44.729491755-07:00"
|
||||
description: High Performance, Kubernetes Native Object Storage
|
||||
digest: 1add7608692cbf39aaf9b1252530e566f7b2f306a14e390b0f49b97a20f2b188
|
||||
home: https://min.io
|
||||
@@ -997,4 +1085,4 @@ entries:
|
||||
urls:
|
||||
- https://charts.min.io/helm-releases/minio-1.0.0.tgz
|
||||
version: 1.0.0
|
||||
generated: "2022-03-03T15:28:46.658942785-08:00"
|
||||
generated: "2022-03-23T21:06:44.728148967-07:00"
|
||||
|
||||
@@ -162,22 +162,6 @@ func (sCfg *Config) UnmarshalJSON(data []byte) error {
|
||||
return json.Unmarshal(data, &aux)
|
||||
}
|
||||
|
||||
// GetListQuorum interprets list quorum values and returns appropriate
|
||||
// acceptable quorum expected for list operations
|
||||
func (sCfg Config) GetListQuorum() int {
|
||||
switch sCfg.ListQuorum {
|
||||
case "reduced":
|
||||
return 2
|
||||
case "disk":
|
||||
// smallest possible value, generally meant for testing.
|
||||
return 1
|
||||
case "strict":
|
||||
return -1
|
||||
}
|
||||
// Defaults to 3 drives per set, defaults to "optimal" value
|
||||
return 3
|
||||
}
|
||||
|
||||
// LookupConfig - lookup api config and override with valid environment settings if any.
|
||||
func LookupConfig(kvs config.KVS) (cfg Config, err error) {
|
||||
// remove this since we have removed this already.
|
||||
|
||||
@@ -62,13 +62,14 @@ const (
|
||||
|
||||
EnvUpdate = "MINIO_UPDATE"
|
||||
|
||||
EnvKMSSecretKey = "MINIO_KMS_SECRET_KEY"
|
||||
EnvKMSSecretKeyFile = "MINIO_KMS_SECRET_KEY_FILE"
|
||||
EnvKESEndpoint = "MINIO_KMS_KES_ENDPOINT"
|
||||
EnvKESKeyName = "MINIO_KMS_KES_KEY_NAME"
|
||||
EnvKESClientKey = "MINIO_KMS_KES_KEY_FILE"
|
||||
EnvKESClientCert = "MINIO_KMS_KES_CERT_FILE"
|
||||
EnvKESServerCA = "MINIO_KMS_KES_CAPATH"
|
||||
EnvKMSSecretKey = "MINIO_KMS_SECRET_KEY"
|
||||
EnvKMSSecretKeyFile = "MINIO_KMS_SECRET_KEY_FILE"
|
||||
EnvKESEndpoint = "MINIO_KMS_KES_ENDPOINT"
|
||||
EnvKESKeyName = "MINIO_KMS_KES_KEY_NAME"
|
||||
EnvKESClientKey = "MINIO_KMS_KES_KEY_FILE"
|
||||
EnvKESClientPassword = "MINIO_KMS_KES_KEY_PASSWORD"
|
||||
EnvKESClientCert = "MINIO_KMS_KES_CERT_FILE"
|
||||
EnvKESServerCA = "MINIO_KMS_KES_CAPATH"
|
||||
|
||||
EnvEndpoints = "MINIO_ENDPOINTS" // legacy
|
||||
EnvWorm = "MINIO_WORM" // legacy
|
||||
|
||||
@@ -155,7 +155,6 @@ func (c *OperatorDNS) DeleteRecord(record SrvRecord) error {
|
||||
|
||||
// Close closes the internal http client
|
||||
func (c *OperatorDNS) Close() error {
|
||||
c.httpClient.CloseIdleConnections()
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
// Copyright (c) 2015-2022 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Result - type for high-level names for the validation status of the config.
|
||||
type Result string
|
||||
|
||||
// Constant values for Result type.
|
||||
const (
|
||||
ConfigOk Result = "Config OK"
|
||||
ConnectivityError Result = "LDAP Server Connection Error"
|
||||
LookupBindError Result = "LDAP Lookup Bind Error"
|
||||
UserSearchParamsMisconfigured Result = "User Search Parameters Misconfigured"
|
||||
GroupSearchParamsMisconfigured Result = "Group Search Parameters Misconfigured"
|
||||
UserDNLookupError Result = "User DN Lookup Error"
|
||||
GroupMembershipsLookupError Result = "Group Memberships Lookup Error"
|
||||
)
|
||||
|
||||
// Validation returns feedback on the configuration. The `Suggestion` field
|
||||
// needs to be "printed" for friendly display (it can contain escaped newlines
|
||||
// `\n`).
|
||||
type Validation struct {
|
||||
Result Result
|
||||
Detail string
|
||||
Suggestion string
|
||||
ErrCause error
|
||||
}
|
||||
|
||||
// Error instance for Validation.
|
||||
func (v Validation) Error() string {
|
||||
if v.Result == ConfigOk {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%s: %s", string(v.Result), v.Detail)
|
||||
}
|
||||
|
||||
// IsOk - returns if the validation succeeded.
|
||||
func (v Validation) IsOk() bool {
|
||||
return v.Result == ConfigOk
|
||||
}
|
||||
|
||||
// UserLookupResult returns the DN found for the test user and their group
|
||||
// memberships.
|
||||
type UserLookupResult struct {
|
||||
DN string
|
||||
GroupDNMemberships []string
|
||||
}
|
||||
|
||||
// Validate validates the LDAP configuration. It can be called with any subset
|
||||
// of configuration parameters provided by the user - it will return
|
||||
// information on what needs to be done to fix the problem if any.
|
||||
//
|
||||
// This function updates the UserDNSearchBaseDistNames and
|
||||
// GroupSearchBaseDistNames fields of the Config - however this an idempotent
|
||||
// operation. This is done to support configuration validation in Console/mc and
|
||||
// for tests.
|
||||
func (l *Config) Validate() Validation {
|
||||
if !l.Enabled {
|
||||
return Validation{Result: ConfigOk, Detail: "Config is not enabled"}
|
||||
}
|
||||
|
||||
if l.ServerAddr == "" {
|
||||
return Validation{
|
||||
Result: ConnectivityError,
|
||||
Detail: "Address is empty",
|
||||
Suggestion: "Set a server address.",
|
||||
}
|
||||
}
|
||||
|
||||
conn, err := l.Connect()
|
||||
if err != nil {
|
||||
return Validation{
|
||||
Result: ConnectivityError,
|
||||
Detail: fmt.Sprintf("Could not connect to LDAP server: %v", err),
|
||||
ErrCause: err,
|
||||
Suggestion: `Check:
|
||||
(1) server address
|
||||
(2) TLS parameters, and
|
||||
(3) LDAP server's TLS certificate is trusted by MinIO (when using TLS - highly recommended)`,
|
||||
}
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
if l.LookupBindDN == "" {
|
||||
return Validation{
|
||||
Result: LookupBindError,
|
||||
Detail: "Lookup Bind UserDN not specified",
|
||||
Suggestion: "Specify LDAP service account credentials for performing lookups.",
|
||||
}
|
||||
}
|
||||
if err := l.lookupBind(conn); err != nil {
|
||||
return Validation{
|
||||
Result: LookupBindError,
|
||||
ErrCause: err,
|
||||
Detail: fmt.Sprintf("Error connecting as LDAP Lookup Bind user: %v", err),
|
||||
Suggestion: "Check LDAP Lookup Bind user credentials and if user is allowed to login",
|
||||
}
|
||||
}
|
||||
|
||||
// Validate User Lookup parameters
|
||||
if l.UserDNSearchBaseDistName == "" {
|
||||
return Validation{
|
||||
Result: UserSearchParamsMisconfigured,
|
||||
Detail: "UserDN search base is empty",
|
||||
Suggestion: "Set the UserDN search base to the DN of the directory subtree where users are present",
|
||||
}
|
||||
}
|
||||
l.UserDNSearchBaseDistNames = strings.Split(l.UserDNSearchBaseDistName, dnDelimiter)
|
||||
|
||||
if l.UserDNSearchFilter == "" {
|
||||
return Validation{
|
||||
Result: UserSearchParamsMisconfigured,
|
||||
Detail: "UserDN search filter is empty",
|
||||
Suggestion: `Set the UserDN search filter template:
|
||||
Use "%s" - it will be replaced by the login user name and sent to the LDAP server.
|
||||
For example: "(uid=%s)"`,
|
||||
}
|
||||
}
|
||||
if strings.Contains(l.UserDNSearchFilter, "%d") {
|
||||
return Validation{
|
||||
Result: UserSearchParamsMisconfigured,
|
||||
Detail: "User DN search filter contains `%d`",
|
||||
Suggestion: `User DN search filter is a template where "%s" is replaced by the login username.
|
||||
"%d" is not supported here.
|
||||
Please provide a search filter containing "%s"`,
|
||||
}
|
||||
}
|
||||
if !strings.Contains(l.UserDNSearchFilter, "%s") {
|
||||
return Validation{
|
||||
Result: UserSearchParamsMisconfigured,
|
||||
Detail: "User DN search filter does not contain `%s`",
|
||||
Suggestion: `During login, the user's DN is looked up using the search filter template:
|
||||
"%s" gets replaced by the given username - it must be used.
|
||||
Enter an LDAP search filter containing "%s"`,
|
||||
}
|
||||
}
|
||||
|
||||
// If group lookup is not configured, it's ok.
|
||||
if l.GroupSearchBaseDistName != "" || l.GroupSearchFilter != "" {
|
||||
|
||||
// Validate Group Search parameters as they are given.
|
||||
if l.GroupSearchBaseDistName == "" {
|
||||
return Validation{
|
||||
Result: GroupSearchParamsMisconfigured,
|
||||
Detail: "Group Search Base DN is required.",
|
||||
Suggestion: `Since you entered a value for the Group Search Filter - enter a value for the Group Search Base DN too:
|
||||
Enter this value as the DN of the subtree where groups will be found.`,
|
||||
}
|
||||
}
|
||||
l.GroupSearchBaseDistNames = strings.Split(l.GroupSearchBaseDistName, dnDelimiter)
|
||||
|
||||
if l.GroupSearchFilter == "" {
|
||||
return Validation{
|
||||
Result: GroupSearchParamsMisconfigured,
|
||||
Detail: "Group Search Filter is required.",
|
||||
Suggestion: `Since you entered a value for the Group Search Base DN - enter a value for the Group Search Filter too. This is a template where, before the query is sent to the server:
|
||||
"%s" is replaced with the login username;
|
||||
"%d" is replaced with the DN of the login user.
|
||||
For example: "(&(objectclass=groupOfNames)(memberUid=%s))"`,
|
||||
}
|
||||
}
|
||||
|
||||
if !strings.Contains(l.GroupSearchFilter, "%d") && !strings.Contains(l.GroupSearchFilter, "%s") {
|
||||
return Validation{
|
||||
Result: GroupSearchParamsMisconfigured,
|
||||
Detail: `GroupSearchFilter must contain at least one of "%s" or "%d"`,
|
||||
Suggestion: `During group membership lookup the group search filter template is used:
|
||||
"%s" gets replaced by the given username, and
|
||||
"%d" gets replaced by the user's DN.
|
||||
Either one is needed to find only groups that the user is a member of.
|
||||
Enter an LDAP search filter template using at least one of these.`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Validation{
|
||||
Result: ConfigOk,
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateLookup takes a test username and performs user and group lookup (if
|
||||
// configured) and returns the result. It is to validate the LDAP configuration.
|
||||
// The lookup is performed without requiring the password for the test user -
|
||||
// and so can be used to test any LDAP user intending to use MinIO.
|
||||
func (l *Config) ValidateLookup(testUsername string) (*UserLookupResult, Validation) {
|
||||
if testUsername == "" {
|
||||
return nil, Validation{
|
||||
Result: UserDNLookupError,
|
||||
Detail: "Provided username is empty",
|
||||
}
|
||||
}
|
||||
|
||||
if r := l.Validate(); !r.IsOk() {
|
||||
return nil, r
|
||||
}
|
||||
|
||||
conn, err := l.Connect()
|
||||
if err != nil {
|
||||
return nil, Validation{
|
||||
Result: ConnectivityError,
|
||||
Detail: fmt.Sprintf("Could not connect to LDAP server: %v", err),
|
||||
ErrCause: err,
|
||||
Suggestion: `Check:
|
||||
(1) server address
|
||||
(2) TLS parameters, and
|
||||
(3) LDAP server's TLS certificate is trusted by MinIO (when using TLS - highly recommended)`,
|
||||
}
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
if err := l.lookupBind(conn); err != nil {
|
||||
return nil, Validation{
|
||||
Result: LookupBindError,
|
||||
ErrCause: err,
|
||||
Detail: fmt.Sprintf("Error connecting as LDAP Lookup Bind user: %v", err),
|
||||
Suggestion: "Check LDAP Lookup Bind user credentials and if user is allowed to login",
|
||||
}
|
||||
}
|
||||
|
||||
// Lookup the given username.
|
||||
dn, err := l.lookupUserDN(conn, testUsername)
|
||||
if err != nil {
|
||||
return nil, Validation{
|
||||
Result: UserDNLookupError,
|
||||
Detail: fmt.Sprintf("Got an error when looking up user (%s) DN: %v", testUsername, err),
|
||||
ErrCause: err,
|
||||
Suggestion: `Check if this is a temporary error and try again.
|
||||
Perhaps there is an error in the user search filter or user search base DN.`,
|
||||
}
|
||||
}
|
||||
|
||||
// Lookup groups.
|
||||
groups, err := l.searchForUserGroups(conn, testUsername, dn)
|
||||
if err != nil {
|
||||
return nil, Validation{
|
||||
Result: GroupMembershipsLookupError,
|
||||
Detail: fmt.Sprintf("Got an error when looking up groups for user(=>%s, dn=>%s): %v", testUsername, dn, err),
|
||||
ErrCause: err,
|
||||
Suggestion: `Check if this is a temporary error and try again.
|
||||
Perhaps there is an error in the group search filter or group search base DN.`,
|
||||
}
|
||||
}
|
||||
|
||||
return &UserLookupResult{
|
||||
DN: dn,
|
||||
GroupDNMemberships: groups,
|
||||
}, Validation{
|
||||
Result: ConfigOk,
|
||||
Detail: "User lookup done.",
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
// Copyright (c) 2015-2022 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/minio-go/v7/pkg/set"
|
||||
)
|
||||
|
||||
const (
|
||||
EnvTestLDAPServer = "LDAP_TEST_SERVER"
|
||||
)
|
||||
|
||||
func TestConfigValidator(t *testing.T) {
|
||||
ldapServer := os.Getenv(EnvTestLDAPServer)
|
||||
if ldapServer == "" {
|
||||
t.Skip()
|
||||
}
|
||||
testCases := []struct {
|
||||
cfg Config
|
||||
expectedResult Result
|
||||
}{
|
||||
{
|
||||
cfg: func() Config {
|
||||
v := Config{Enabled: true}
|
||||
return v
|
||||
}(),
|
||||
expectedResult: ConnectivityError,
|
||||
},
|
||||
{
|
||||
cfg: func() Config {
|
||||
v := Config{Enabled: true}
|
||||
v.ServerAddr = ldapServer
|
||||
return v
|
||||
}(),
|
||||
expectedResult: ConnectivityError,
|
||||
},
|
||||
{
|
||||
cfg: func() Config {
|
||||
v := Config{Enabled: true}
|
||||
v.ServerAddr = ldapServer
|
||||
v.serverInsecure = true
|
||||
return v
|
||||
}(),
|
||||
expectedResult: LookupBindError,
|
||||
},
|
||||
{
|
||||
cfg: func() Config {
|
||||
v := Config{Enabled: true}
|
||||
v.ServerAddr = ldapServer
|
||||
v.serverInsecure = true
|
||||
v.LookupBindDN = "cn=admin,dc=min,dc=io"
|
||||
v.LookupBindPassword = "admin1"
|
||||
return v
|
||||
}(),
|
||||
expectedResult: LookupBindError,
|
||||
},
|
||||
{ // Case 4
|
||||
cfg: func() Config {
|
||||
v := Config{Enabled: true}
|
||||
v.ServerAddr = ldapServer
|
||||
v.serverInsecure = true
|
||||
v.LookupBindDN = "cn=admin,dc=min,dc=io"
|
||||
v.LookupBindPassword = "admin"
|
||||
return v
|
||||
}(),
|
||||
expectedResult: UserSearchParamsMisconfigured,
|
||||
},
|
||||
{
|
||||
cfg: func() Config {
|
||||
v := Config{Enabled: true}
|
||||
v.ServerAddr = ldapServer
|
||||
v.serverInsecure = true
|
||||
v.LookupBindDN = "cn=admin,dc=min,dc=io"
|
||||
v.LookupBindPassword = "admin"
|
||||
v.UserDNSearchFilter = "(uid=x)"
|
||||
v.UserDNSearchBaseDistName = "dc=min,dc=io"
|
||||
return v
|
||||
}(),
|
||||
expectedResult: UserSearchParamsMisconfigured,
|
||||
},
|
||||
{
|
||||
cfg: func() Config {
|
||||
v := Config{Enabled: true}
|
||||
v.ServerAddr = ldapServer
|
||||
v.serverInsecure = true
|
||||
v.LookupBindDN = "cn=admin,dc=min,dc=io"
|
||||
v.LookupBindPassword = "admin"
|
||||
v.UserDNSearchFilter = "(uid=%s)"
|
||||
v.UserDNSearchBaseDistName = "dc=min,dc=io"
|
||||
return v
|
||||
}(),
|
||||
expectedResult: ConfigOk,
|
||||
},
|
||||
{ // Case 7
|
||||
cfg: func() Config {
|
||||
v := Config{Enabled: true}
|
||||
v.ServerAddr = ldapServer
|
||||
v.serverInsecure = true
|
||||
v.LookupBindDN = "cn=admin,dc=min,dc=io"
|
||||
v.LookupBindPassword = "admin"
|
||||
v.UserDNSearchFilter = "(uid=%s)"
|
||||
v.UserDNSearchBaseDistName = "dc=min,dc=io"
|
||||
v.GroupSearchBaseDistName = "ou=swengg,dc=min,dc=io"
|
||||
v.GroupSearchFilter = "(&(objectclass=groupofnames)(member=x))"
|
||||
return v
|
||||
}(),
|
||||
expectedResult: GroupSearchParamsMisconfigured,
|
||||
},
|
||||
{
|
||||
cfg: func() Config {
|
||||
v := Config{Enabled: true}
|
||||
v.ServerAddr = ldapServer
|
||||
v.serverInsecure = true
|
||||
v.LookupBindDN = "cn=admin,dc=min,dc=io"
|
||||
v.LookupBindPassword = "admin"
|
||||
v.UserDNSearchFilter = "(uid=%s)"
|
||||
v.UserDNSearchBaseDistName = "dc=min,dc=io"
|
||||
v.GroupSearchFilter = "(&(objectclass=groupofnames)(member=x))"
|
||||
return v
|
||||
}(),
|
||||
expectedResult: GroupSearchParamsMisconfigured,
|
||||
},
|
||||
{ // Case 9
|
||||
cfg: func() Config {
|
||||
v := Config{Enabled: true}
|
||||
v.ServerAddr = ldapServer
|
||||
v.serverInsecure = true
|
||||
v.LookupBindDN = "cn=admin,dc=min,dc=io"
|
||||
v.LookupBindPassword = "admin"
|
||||
v.UserDNSearchFilter = "(uid=%s)"
|
||||
v.UserDNSearchBaseDistName = "dc=min,dc=io"
|
||||
v.GroupSearchBaseDistName = "ou=swengg,dc=min,dc=io"
|
||||
v.GroupSearchFilter = "(&(objectclass=groupofnames)(member=%d))"
|
||||
return v
|
||||
}(),
|
||||
expectedResult: ConfigOk,
|
||||
},
|
||||
}
|
||||
|
||||
expectedDN := "uid=dillon,ou=people,ou=swengg,dc=min,dc=io"
|
||||
expectedGroups := set.CreateStringSet(
|
||||
"cn=projecta,ou=groups,ou=swengg,dc=min,dc=io",
|
||||
"cn=projectb,ou=groups,ou=swengg,dc=min,dc=io",
|
||||
)
|
||||
|
||||
for i, test := range testCases {
|
||||
result := test.cfg.Validate()
|
||||
if result.Result != test.expectedResult {
|
||||
fmt.Printf("Result: %#v\n", result)
|
||||
t.Fatalf("Case %d: Got `%s` expected `%s`", i, result.Result, string(test.expectedResult))
|
||||
}
|
||||
if result.IsOk() {
|
||||
lookupResult, validationResult := test.cfg.ValidateLookup("dillon")
|
||||
if !validationResult.IsOk() {
|
||||
t.Fatalf("Case %d: Got unexpected validation failure: %#v\n", i, validationResult)
|
||||
}
|
||||
if lookupResult.DN != expectedDN {
|
||||
t.Fatalf("Case %d: Got unexpected DN: %v", i, lookupResult.DN)
|
||||
}
|
||||
|
||||
if test.cfg.GroupSearchFilter == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if !set.CreateStringSet(lookupResult.GroupDNMemberships...).Equals(expectedGroups) {
|
||||
t.Fatalf("Case %d: Got unexpected groups: %v", i, lookupResult.GroupDNMemberships)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,18 +18,9 @@
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
ldap "github.com/go-ldap/ldap/v3"
|
||||
"github.com/minio/minio-go/v7/pkg/set"
|
||||
"github.com/minio/minio/internal/auth"
|
||||
"github.com/minio/minio/internal/config"
|
||||
"github.com/minio/pkg/env"
|
||||
)
|
||||
@@ -52,12 +43,12 @@ type Config struct {
|
||||
|
||||
// User DN search parameters
|
||||
UserDNSearchBaseDistName string `json:"userDNSearchBaseDN"`
|
||||
UserDNSearchBaseDistNames []string `json:"-"`
|
||||
UserDNSearchBaseDistNames []string `json:"-"` // Generated field
|
||||
UserDNSearchFilter string `json:"userDNSearchFilter"`
|
||||
|
||||
// Group search parameters
|
||||
GroupSearchBaseDistName string `json:"groupSearchBaseDN"`
|
||||
GroupSearchBaseDistNames []string `json:"-"`
|
||||
GroupSearchBaseDistNames []string `json:"-"` // Generated field
|
||||
GroupSearchFilter string `json:"groupSearchFilter"`
|
||||
|
||||
// Lookup bind LDAP service account
|
||||
@@ -151,318 +142,6 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
func getGroups(conn *ldap.Conn, sreq *ldap.SearchRequest) ([]string, error) {
|
||||
var groups []string
|
||||
sres, err := conn.Search(sreq)
|
||||
if err != nil {
|
||||
// Check if there is no matching result and return empty slice.
|
||||
// Ref: https://ldap.com/ldap-result-code-reference/
|
||||
if ldap.IsErrorWithCode(err, 32) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
for _, entry := range sres.Entries {
|
||||
// We only queried one attribute,
|
||||
// so we only look up the first one.
|
||||
groups = append(groups, entry.DN)
|
||||
}
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
func (l *Config) lookupBind(conn *ldap.Conn) error {
|
||||
var err error
|
||||
if l.LookupBindPassword == "" {
|
||||
err = conn.UnauthenticatedBind(l.LookupBindDN)
|
||||
} else {
|
||||
err = conn.Bind(l.LookupBindDN, l.LookupBindPassword)
|
||||
}
|
||||
if ldap.IsErrorWithCode(err, 49) {
|
||||
return fmt.Errorf("LDAP Lookup Bind user invalid credentials error: %w", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// lookupUserDN searches for the DN of the user given their username. conn is
|
||||
// assumed to be using the lookup bind service account. It is required that the
|
||||
// search result in at most one result.
|
||||
func (l *Config) lookupUserDN(conn *ldap.Conn, username string) (string, error) {
|
||||
filter := strings.ReplaceAll(l.UserDNSearchFilter, "%s", ldap.EscapeFilter(username))
|
||||
var foundDistNames []string
|
||||
for _, userSearchBase := range l.UserDNSearchBaseDistNames {
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
userSearchBase,
|
||||
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
||||
filter,
|
||||
[]string{}, // only need DN, so no pass no attributes here
|
||||
nil,
|
||||
)
|
||||
|
||||
searchResult, err := conn.Search(searchRequest)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, entry := range searchResult.Entries {
|
||||
foundDistNames = append(foundDistNames, entry.DN)
|
||||
}
|
||||
}
|
||||
if len(foundDistNames) == 0 {
|
||||
return "", fmt.Errorf("User DN for %s not found", username)
|
||||
}
|
||||
if len(foundDistNames) != 1 {
|
||||
return "", fmt.Errorf("Multiple DNs for %s found - please fix the search filter", username)
|
||||
}
|
||||
return foundDistNames[0], nil
|
||||
}
|
||||
|
||||
func (l *Config) searchForUserGroups(conn *ldap.Conn, username, bindDN string) ([]string, error) {
|
||||
// User groups lookup.
|
||||
var groups []string
|
||||
if l.GroupSearchFilter != "" {
|
||||
for _, groupSearchBase := range l.GroupSearchBaseDistNames {
|
||||
filter := strings.ReplaceAll(l.GroupSearchFilter, "%s", ldap.EscapeFilter(username))
|
||||
filter = strings.ReplaceAll(filter, "%d", ldap.EscapeFilter(bindDN))
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
groupSearchBase,
|
||||
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
||||
filter,
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
|
||||
var newGroups []string
|
||||
newGroups, err := getGroups(conn, searchRequest)
|
||||
if err != nil {
|
||||
errRet := fmt.Errorf("Error finding groups of %s: %w", bindDN, err)
|
||||
return nil, errRet
|
||||
}
|
||||
|
||||
groups = append(groups, newGroups...)
|
||||
}
|
||||
}
|
||||
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
// LookupUserDN searches for the full DN and groups of a given username
|
||||
func (l *Config) LookupUserDN(username string) (string, []string, error) {
|
||||
conn, err := l.Connect()
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Bind to the lookup user account
|
||||
if err = l.lookupBind(conn); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
// Lookup user DN
|
||||
bindDN, err := l.lookupUserDN(conn, username)
|
||||
if err != nil {
|
||||
errRet := fmt.Errorf("Unable to find user DN: %w", err)
|
||||
return "", nil, errRet
|
||||
}
|
||||
|
||||
groups, err := l.searchForUserGroups(conn, username, bindDN)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
return bindDN, groups, nil
|
||||
}
|
||||
|
||||
// Bind - binds to ldap, searches LDAP and returns the distinguished name of the
|
||||
// user and the list of groups.
|
||||
func (l *Config) Bind(username, password string) (string, []string, error) {
|
||||
conn, err := l.Connect()
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
var bindDN string
|
||||
// Bind to the lookup user account
|
||||
if err = l.lookupBind(conn); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
// Lookup user DN
|
||||
bindDN, err = l.lookupUserDN(conn, username)
|
||||
if err != nil {
|
||||
errRet := fmt.Errorf("Unable to find user DN: %w", err)
|
||||
return "", nil, errRet
|
||||
}
|
||||
|
||||
// Authenticate the user credentials.
|
||||
err = conn.Bind(bindDN, password)
|
||||
if err != nil {
|
||||
errRet := fmt.Errorf("LDAP auth failed for DN %s: %w", bindDN, err)
|
||||
return "", nil, errRet
|
||||
}
|
||||
|
||||
// Bind to the lookup user account again to perform group search.
|
||||
if err = l.lookupBind(conn); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
// User groups lookup.
|
||||
groups, err := l.searchForUserGroups(conn, username, bindDN)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
return bindDN, groups, nil
|
||||
}
|
||||
|
||||
// Connect connect to ldap server.
|
||||
func (l *Config) Connect() (ldapConn *ldap.Conn, err error) {
|
||||
if l == nil {
|
||||
return nil, errors.New("LDAP is not configured")
|
||||
}
|
||||
|
||||
_, _, err = net.SplitHostPort(l.ServerAddr)
|
||||
if err != nil {
|
||||
// User default LDAP port if none specified "636"
|
||||
l.ServerAddr = net.JoinHostPort(l.ServerAddr, "636")
|
||||
}
|
||||
|
||||
if l.serverInsecure {
|
||||
return ldap.Dial("tcp", l.ServerAddr)
|
||||
}
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
InsecureSkipVerify: l.tlsSkipVerify,
|
||||
RootCAs: l.rootCAs,
|
||||
}
|
||||
|
||||
if l.serverStartTLS {
|
||||
conn, err := ldap.Dial("tcp", l.ServerAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = conn.StartTLS(tlsConfig)
|
||||
return conn, err
|
||||
}
|
||||
|
||||
return ldap.DialTLS("tcp", l.ServerAddr, tlsConfig)
|
||||
}
|
||||
|
||||
// GetExpiryDuration - return parsed expiry duration.
|
||||
func (l Config) GetExpiryDuration(dsecs string) (time.Duration, error) {
|
||||
if dsecs == "" {
|
||||
return l.stsExpiryDuration, nil
|
||||
}
|
||||
|
||||
d, err := strconv.Atoi(dsecs)
|
||||
if err != nil {
|
||||
return 0, auth.ErrInvalidDuration
|
||||
}
|
||||
|
||||
dur := time.Duration(d) * time.Second
|
||||
|
||||
if dur < minLDAPExpiry || dur > maxLDAPExpiry {
|
||||
return 0, auth.ErrInvalidDuration
|
||||
}
|
||||
return dur, nil
|
||||
}
|
||||
|
||||
func (l Config) testConnection() error {
|
||||
conn, err := l.Connect()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error creating connection to LDAP server: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
if err = l.lookupBind(conn); err != nil {
|
||||
return fmt.Errorf("Error connecting as LDAP Lookup Bind user: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsLDAPUserDN determines if the given string could be a user DN from LDAP.
|
||||
func (l Config) IsLDAPUserDN(user string) bool {
|
||||
for _, baseDN := range l.UserDNSearchBaseDistNames {
|
||||
if strings.HasSuffix(user, ","+baseDN) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetNonEligibleUserDistNames - find user accounts (DNs) that are no longer
|
||||
// present in the LDAP server or do not meet filter criteria anymore
|
||||
func (l *Config) GetNonEligibleUserDistNames(userDistNames []string) ([]string, error) {
|
||||
conn, err := l.Connect()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Bind to the lookup user account
|
||||
if err = l.lookupBind(conn); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Evaluate the filter again with generic wildcard instead of specific values
|
||||
filter := strings.ReplaceAll(l.UserDNSearchFilter, "%s", "*")
|
||||
|
||||
nonExistentUsers := []string{}
|
||||
for _, dn := range userDistNames {
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
dn,
|
||||
ldap.ScopeBaseObject, ldap.NeverDerefAliases, 0, 0, false,
|
||||
filter,
|
||||
[]string{}, // only need DN, so pass no attributes here
|
||||
nil,
|
||||
)
|
||||
|
||||
searchResult, err := conn.Search(searchRequest)
|
||||
if err != nil {
|
||||
// Object does not exist error?
|
||||
if ldap.IsErrorWithCode(err, 32) {
|
||||
nonExistentUsers = append(nonExistentUsers, dn)
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if len(searchResult.Entries) == 0 {
|
||||
// DN was not found - this means this user account is
|
||||
// expired.
|
||||
nonExistentUsers = append(nonExistentUsers, dn)
|
||||
}
|
||||
}
|
||||
return nonExistentUsers, nil
|
||||
}
|
||||
|
||||
// LookupGroupMemberships - for each DN finds the set of LDAP groups they are a
|
||||
// member of.
|
||||
func (l *Config) LookupGroupMemberships(userDistNames []string, userDNToUsernameMap map[string]string) (map[string]set.StringSet, error) {
|
||||
conn, err := l.Connect()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Bind to the lookup user account
|
||||
if err = l.lookupBind(conn); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := make(map[string]set.StringSet, len(userDistNames))
|
||||
for _, userDistName := range userDistNames {
|
||||
username := userDNToUsernameMap[userDistName]
|
||||
groups, err := l.searchForUserGroups(conn, username, userDistName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res[userDistName] = set.CreateStringSet(groups...)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// Enabled returns if LDAP config is enabled.
|
||||
func Enabled(kvs config.KVS) bool {
|
||||
return kvs.Get(ServerAddr) != ""
|
||||
@@ -480,6 +159,7 @@ func Lookup(kvs config.KVS, rootCAs *x509.CertPool) (l Config, err error) {
|
||||
if err = config.CheckValidKeys(config.IdentityLDAPSubSys, kvs, DefaultKVS); err != nil {
|
||||
return l, err
|
||||
}
|
||||
|
||||
ldapServer := env.Get(EnvServerAddr, kvs.Get(ServerAddr))
|
||||
if ldapServer == "" {
|
||||
return l, nil
|
||||
@@ -510,44 +190,21 @@ func Lookup(kvs config.KVS, rootCAs *x509.CertPool) (l Config, err error) {
|
||||
}
|
||||
|
||||
// Lookup bind user configuration
|
||||
lookupBindDN := env.Get(EnvLookupBindDN, kvs.Get(LookupBindDN))
|
||||
if lookupBindDN == "" {
|
||||
return l, errors.New("Lookup Bind DN is required")
|
||||
}
|
||||
lookupBindPassword := env.Get(EnvLookupBindPassword, kvs.Get(LookupBindPassword))
|
||||
if lookupBindDN != "" {
|
||||
l.LookupBindDN = lookupBindDN
|
||||
l.LookupBindPassword = lookupBindPassword
|
||||
}
|
||||
|
||||
// Test connection to LDAP server.
|
||||
if err := l.testConnection(); err != nil {
|
||||
return l, fmt.Errorf("Connection test for LDAP server failed: %w", err)
|
||||
}
|
||||
l.LookupBindDN = env.Get(EnvLookupBindDN, kvs.Get(LookupBindDN))
|
||||
l.LookupBindPassword = env.Get(EnvLookupBindPassword, kvs.Get(LookupBindPassword))
|
||||
|
||||
// User DN search configuration
|
||||
userDNSearchBaseDN := env.Get(EnvUserDNSearchBaseDN, kvs.Get(UserDNSearchBaseDN))
|
||||
userDNSearchFilter := env.Get(EnvUserDNSearchFilter, kvs.Get(UserDNSearchFilter))
|
||||
if userDNSearchFilter == "" || userDNSearchBaseDN == "" {
|
||||
return l, errors.New("UserDN search base DN and UserDN search filter are both required")
|
||||
}
|
||||
l.UserDNSearchBaseDistName = userDNSearchBaseDN
|
||||
l.UserDNSearchBaseDistNames = strings.Split(userDNSearchBaseDN, dnDelimiter)
|
||||
l.UserDNSearchFilter = userDNSearchFilter
|
||||
l.UserDNSearchFilter = env.Get(EnvUserDNSearchFilter, kvs.Get(UserDNSearchFilter))
|
||||
l.UserDNSearchBaseDistName = env.Get(EnvUserDNSearchBaseDN, kvs.Get(UserDNSearchBaseDN))
|
||||
|
||||
// Group search params configuration
|
||||
grpSearchFilter := env.Get(EnvGroupSearchFilter, kvs.Get(GroupSearchFilter))
|
||||
grpSearchBaseDN := env.Get(EnvGroupSearchBaseDN, kvs.Get(GroupSearchBaseDN))
|
||||
l.GroupSearchFilter = env.Get(EnvGroupSearchFilter, kvs.Get(GroupSearchFilter))
|
||||
l.GroupSearchBaseDistName = env.Get(EnvGroupSearchBaseDN, kvs.Get(GroupSearchBaseDN))
|
||||
|
||||
// Either all group params must be set or none must be set.
|
||||
if (grpSearchFilter != "" && grpSearchBaseDN == "") || (grpSearchFilter == "" && grpSearchBaseDN != "") {
|
||||
return l, errors.New("All group related parameters must be set")
|
||||
}
|
||||
|
||||
if grpSearchFilter != "" {
|
||||
l.GroupSearchFilter = grpSearchFilter
|
||||
l.GroupSearchBaseDistName = grpSearchBaseDN
|
||||
l.GroupSearchBaseDistNames = strings.Split(l.GroupSearchBaseDistName, dnDelimiter)
|
||||
// Validate and test configuration.
|
||||
valResult := l.Validate()
|
||||
if !valResult.IsOk() {
|
||||
return l, valResult
|
||||
}
|
||||
|
||||
return l, nil
|
||||
|
||||
@@ -0,0 +1,331 @@
|
||||
// Copyright (c) 2015-2022 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
ldap "github.com/go-ldap/ldap/v3"
|
||||
"github.com/minio/minio-go/v7/pkg/set"
|
||||
"github.com/minio/minio/internal/auth"
|
||||
)
|
||||
|
||||
func getGroups(conn *ldap.Conn, sreq *ldap.SearchRequest) ([]string, error) {
|
||||
var groups []string
|
||||
sres, err := conn.Search(sreq)
|
||||
if err != nil {
|
||||
// Check if there is no matching result and return empty slice.
|
||||
// Ref: https://ldap.com/ldap-result-code-reference/
|
||||
if ldap.IsErrorWithCode(err, 32) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
for _, entry := range sres.Entries {
|
||||
// We only queried one attribute,
|
||||
// so we only look up the first one.
|
||||
groups = append(groups, entry.DN)
|
||||
}
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
func (l *Config) lookupBind(conn *ldap.Conn) error {
|
||||
var err error
|
||||
if l.LookupBindPassword == "" {
|
||||
err = conn.UnauthenticatedBind(l.LookupBindDN)
|
||||
} else {
|
||||
err = conn.Bind(l.LookupBindDN, l.LookupBindPassword)
|
||||
}
|
||||
if ldap.IsErrorWithCode(err, 49) {
|
||||
return fmt.Errorf("LDAP Lookup Bind user invalid credentials error: %w", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// lookupUserDN searches for the DN of the user given their username. conn is
|
||||
// assumed to be using the lookup bind service account. It is required that the
|
||||
// search result in at most one result.
|
||||
func (l *Config) lookupUserDN(conn *ldap.Conn, username string) (string, error) {
|
||||
filter := strings.ReplaceAll(l.UserDNSearchFilter, "%s", ldap.EscapeFilter(username))
|
||||
var foundDistNames []string
|
||||
for _, userSearchBase := range l.UserDNSearchBaseDistNames {
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
userSearchBase,
|
||||
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
||||
filter,
|
||||
[]string{}, // only need DN, so no pass no attributes here
|
||||
nil,
|
||||
)
|
||||
|
||||
searchResult, err := conn.Search(searchRequest)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, entry := range searchResult.Entries {
|
||||
foundDistNames = append(foundDistNames, entry.DN)
|
||||
}
|
||||
}
|
||||
if len(foundDistNames) == 0 {
|
||||
return "", fmt.Errorf("User DN for %s not found", username)
|
||||
}
|
||||
if len(foundDistNames) != 1 {
|
||||
return "", fmt.Errorf("Multiple DNs for %s found - please fix the search filter", username)
|
||||
}
|
||||
return foundDistNames[0], nil
|
||||
}
|
||||
|
||||
func (l *Config) searchForUserGroups(conn *ldap.Conn, username, bindDN string) ([]string, error) {
|
||||
// User groups lookup.
|
||||
var groups []string
|
||||
if l.GroupSearchFilter != "" {
|
||||
for _, groupSearchBase := range l.GroupSearchBaseDistNames {
|
||||
filter := strings.ReplaceAll(l.GroupSearchFilter, "%s", ldap.EscapeFilter(username))
|
||||
filter = strings.ReplaceAll(filter, "%d", ldap.EscapeFilter(bindDN))
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
groupSearchBase,
|
||||
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
||||
filter,
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
|
||||
var newGroups []string
|
||||
newGroups, err := getGroups(conn, searchRequest)
|
||||
if err != nil {
|
||||
errRet := fmt.Errorf("Error finding groups of %s: %w", bindDN, err)
|
||||
return nil, errRet
|
||||
}
|
||||
|
||||
groups = append(groups, newGroups...)
|
||||
}
|
||||
}
|
||||
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
// LookupUserDN searches for the full DN and groups of a given username
|
||||
func (l *Config) LookupUserDN(username string) (string, []string, error) {
|
||||
conn, err := l.Connect()
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Bind to the lookup user account
|
||||
if err = l.lookupBind(conn); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
// Lookup user DN
|
||||
bindDN, err := l.lookupUserDN(conn, username)
|
||||
if err != nil {
|
||||
errRet := fmt.Errorf("Unable to find user DN: %w", err)
|
||||
return "", nil, errRet
|
||||
}
|
||||
|
||||
groups, err := l.searchForUserGroups(conn, username, bindDN)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
return bindDN, groups, nil
|
||||
}
|
||||
|
||||
// Bind - binds to ldap, searches LDAP and returns the distinguished name of the
|
||||
// user and the list of groups.
|
||||
func (l *Config) Bind(username, password string) (string, []string, error) {
|
||||
conn, err := l.Connect()
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
var bindDN string
|
||||
// Bind to the lookup user account
|
||||
if err = l.lookupBind(conn); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
// Lookup user DN
|
||||
bindDN, err = l.lookupUserDN(conn, username)
|
||||
if err != nil {
|
||||
errRet := fmt.Errorf("Unable to find user DN: %w", err)
|
||||
return "", nil, errRet
|
||||
}
|
||||
|
||||
// Authenticate the user credentials.
|
||||
err = conn.Bind(bindDN, password)
|
||||
if err != nil {
|
||||
errRet := fmt.Errorf("LDAP auth failed for DN %s: %w", bindDN, err)
|
||||
return "", nil, errRet
|
||||
}
|
||||
|
||||
// Bind to the lookup user account again to perform group search.
|
||||
if err = l.lookupBind(conn); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
// User groups lookup.
|
||||
groups, err := l.searchForUserGroups(conn, username, bindDN)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
return bindDN, groups, nil
|
||||
}
|
||||
|
||||
// Connect connect to ldap server.
|
||||
func (l *Config) Connect() (ldapConn *ldap.Conn, err error) {
|
||||
if l == nil {
|
||||
return nil, errors.New("LDAP is not configured")
|
||||
}
|
||||
|
||||
_, _, err = net.SplitHostPort(l.ServerAddr)
|
||||
if err != nil {
|
||||
// User default LDAP port if none specified "636"
|
||||
l.ServerAddr = net.JoinHostPort(l.ServerAddr, "636")
|
||||
}
|
||||
|
||||
if l.serverInsecure {
|
||||
return ldap.Dial("tcp", l.ServerAddr)
|
||||
}
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
InsecureSkipVerify: l.tlsSkipVerify,
|
||||
RootCAs: l.rootCAs,
|
||||
}
|
||||
|
||||
if l.serverStartTLS {
|
||||
conn, err := ldap.Dial("tcp", l.ServerAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = conn.StartTLS(tlsConfig)
|
||||
return conn, err
|
||||
}
|
||||
|
||||
return ldap.DialTLS("tcp", l.ServerAddr, tlsConfig)
|
||||
}
|
||||
|
||||
// GetExpiryDuration - return parsed expiry duration.
|
||||
func (l Config) GetExpiryDuration(dsecs string) (time.Duration, error) {
|
||||
if dsecs == "" {
|
||||
return l.stsExpiryDuration, nil
|
||||
}
|
||||
|
||||
d, err := strconv.Atoi(dsecs)
|
||||
if err != nil {
|
||||
return 0, auth.ErrInvalidDuration
|
||||
}
|
||||
|
||||
dur := time.Duration(d) * time.Second
|
||||
|
||||
if dur < minLDAPExpiry || dur > maxLDAPExpiry {
|
||||
return 0, auth.ErrInvalidDuration
|
||||
}
|
||||
return dur, nil
|
||||
}
|
||||
|
||||
// IsLDAPUserDN determines if the given string could be a user DN from LDAP.
|
||||
func (l Config) IsLDAPUserDN(user string) bool {
|
||||
for _, baseDN := range l.UserDNSearchBaseDistNames {
|
||||
if strings.HasSuffix(user, ","+baseDN) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetNonEligibleUserDistNames - find user accounts (DNs) that are no longer
|
||||
// present in the LDAP server or do not meet filter criteria anymore
|
||||
func (l *Config) GetNonEligibleUserDistNames(userDistNames []string) ([]string, error) {
|
||||
conn, err := l.Connect()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Bind to the lookup user account
|
||||
if err = l.lookupBind(conn); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Evaluate the filter again with generic wildcard instead of specific values
|
||||
filter := strings.ReplaceAll(l.UserDNSearchFilter, "%s", "*")
|
||||
|
||||
nonExistentUsers := []string{}
|
||||
for _, dn := range userDistNames {
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
dn,
|
||||
ldap.ScopeBaseObject, ldap.NeverDerefAliases, 0, 0, false,
|
||||
filter,
|
||||
[]string{}, // only need DN, so pass no attributes here
|
||||
nil,
|
||||
)
|
||||
|
||||
searchResult, err := conn.Search(searchRequest)
|
||||
if err != nil {
|
||||
// Object does not exist error?
|
||||
if ldap.IsErrorWithCode(err, 32) {
|
||||
nonExistentUsers = append(nonExistentUsers, dn)
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if len(searchResult.Entries) == 0 {
|
||||
// DN was not found - this means this user account is
|
||||
// expired.
|
||||
nonExistentUsers = append(nonExistentUsers, dn)
|
||||
}
|
||||
}
|
||||
return nonExistentUsers, nil
|
||||
}
|
||||
|
||||
// LookupGroupMemberships - for each DN finds the set of LDAP groups they are a
|
||||
// member of.
|
||||
func (l *Config) LookupGroupMemberships(userDistNames []string, userDNToUsernameMap map[string]string) (map[string]set.StringSet, error) {
|
||||
conn, err := l.Connect()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Bind to the lookup user account
|
||||
if err = l.lookupBind(conn); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := make(map[string]set.StringSet, len(userDistNames))
|
||||
for _, userDistName := range userDistNames {
|
||||
username := userDNToUsernameMap[userDistName]
|
||||
groups, err := l.searchForUserGroups(conn, username, userDistName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res[userDistName] = set.CreateStringSet(groups...)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
@@ -471,7 +471,6 @@ func parseDiscoveryDoc(u *xnet.URL, transport *http.Transport, closeRespFn func(
|
||||
}
|
||||
resp, err := clnt.Do(req)
|
||||
if err != nil {
|
||||
clnt.CloseIdleConnections()
|
||||
return d, err
|
||||
}
|
||||
defer closeRespFn(resp.Body)
|
||||
|
||||
@@ -96,6 +96,10 @@ func (ssekms) IsEncrypted(metadata map[string]string) bool {
|
||||
// from the metadata using KMS and returns the decrypted object
|
||||
// key.
|
||||
func (s3 ssekms) UnsealObjectKey(KMS kms.KMS, metadata map[string]string, bucket, object string) (key ObjectKey, err error) {
|
||||
if KMS == nil {
|
||||
return key, Errorf("KMS not configured")
|
||||
}
|
||||
|
||||
keyID, kmsKey, sealedKey, ctx, err := s3.ParseMetadata(metadata)
|
||||
if err != nil {
|
||||
return key, err
|
||||
|
||||
@@ -72,6 +72,9 @@ func (sses3) IsEncrypted(metadata map[string]string) bool {
|
||||
// from the metadata using KMS and returns the decrypted object
|
||||
// key.
|
||||
func (s3 sses3) UnsealObjectKey(KMS kms.KMS, metadata map[string]string, bucket, object string) (key ObjectKey, err error) {
|
||||
if KMS == nil {
|
||||
return key, Errorf("KMS not configured")
|
||||
}
|
||||
keyID, kmsKey, sealedKey, err := s3.ParseMetadata(metadata)
|
||||
if err != nil {
|
||||
return key, err
|
||||
@@ -84,6 +87,67 @@ func (s3 sses3) UnsealObjectKey(KMS kms.KMS, metadata map[string]string, bucket,
|
||||
return key, err
|
||||
}
|
||||
|
||||
// UnsealObjectsKeys extracts and decrypts all sealed object keys
|
||||
// from the metadata using the KMS and returns the decrypted object
|
||||
// keys.
|
||||
//
|
||||
// The metadata, buckets and objects slices must have the same length.
|
||||
func (s3 sses3) UnsealObjectKeys(KMS kms.KMS, metadata []map[string]string, buckets, objects []string) ([]ObjectKey, error) {
|
||||
if KMS == nil {
|
||||
return nil, Errorf("KMS not configured")
|
||||
}
|
||||
|
||||
if len(metadata) != len(buckets) || len(metadata) != len(objects) {
|
||||
return nil, Errorf("invalid metadata/object count: %d != %d != %d", len(metadata), len(buckets), len(objects))
|
||||
}
|
||||
|
||||
keyIDs := make([]string, 0, len(metadata))
|
||||
kmsKeys := make([][]byte, 0, len(metadata))
|
||||
sealedKeys := make([]SealedKey, 0, len(metadata))
|
||||
|
||||
sameKeyID := true
|
||||
for i := range metadata {
|
||||
keyID, kmsKey, sealedKey, err := s3.ParseMetadata(metadata[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keyIDs = append(keyIDs, keyID)
|
||||
kmsKeys = append(kmsKeys, kmsKey)
|
||||
sealedKeys = append(sealedKeys, sealedKey)
|
||||
|
||||
if i > 0 && keyID != keyIDs[i-1] {
|
||||
sameKeyID = false
|
||||
}
|
||||
}
|
||||
if sameKeyID {
|
||||
contexts := make([]kms.Context, 0, len(keyIDs))
|
||||
for i := range buckets {
|
||||
contexts = append(contexts, kms.Context{buckets[i]: path.Join(buckets[i], objects[i])})
|
||||
}
|
||||
unsealKeys, err := KMS.DecryptAll(keyIDs[0], kmsKeys, contexts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keys := make([]ObjectKey, len(unsealKeys))
|
||||
for i := range keys {
|
||||
if err := keys[i].Unseal(unsealKeys[i], sealedKeys[i], s3.String(), buckets[i], objects[i]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
keys := make([]ObjectKey, 0, len(keyIDs))
|
||||
for i := range keyIDs {
|
||||
key, err := s3.UnsealObjectKey(KMS, metadata[i], buckets[i], objects[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keys = append(keys, key)
|
||||
}
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
// CreateMetadata encodes the sealed object key into the metadata and returns
|
||||
// the modified metadata. If the keyID and the kmsKey is not empty it encodes
|
||||
// both into the metadata as well. It allocates a new metadata map if metadata
|
||||
|
||||
+17
-2
@@ -143,7 +143,22 @@ func (e ETag) String() string {
|
||||
|
||||
// IsEncrypted reports whether the ETag is encrypted.
|
||||
func (e ETag) IsEncrypted() bool {
|
||||
return len(e) > 16 && !bytes.ContainsRune(e, '-')
|
||||
// An encrypted ETag must be at least 32 bytes long.
|
||||
// It contains the encrypted ETag value + an authentication
|
||||
// code generated by the AEAD cipher.
|
||||
//
|
||||
// Here is an incorrect implementation of IsEncrypted:
|
||||
//
|
||||
// return len(e) > 16 && !bytes.ContainsRune(e, '-')
|
||||
//
|
||||
// An encrypted ETag may contain some random bytes - e.g.
|
||||
// and nonce value. This nonce value may contain a '-'
|
||||
// just by its nature of being randomly generated.
|
||||
// The above implementation would incorrectly consider
|
||||
// such an ETag (with a nonce value containing a '-')
|
||||
// as non-encrypted.
|
||||
|
||||
return len(e) >= 32 // We consider all ETags longer than 32 bytes as encrypted
|
||||
}
|
||||
|
||||
// IsMultipart reports whether the ETag belongs to an
|
||||
@@ -151,7 +166,7 @@ func (e ETag) IsEncrypted() bool {
|
||||
// API.
|
||||
// An S3 multipart ETag has a -<part-number> suffix.
|
||||
func (e ETag) IsMultipart() bool {
|
||||
return len(e) > 16 && bytes.ContainsRune(e, '-')
|
||||
return len(e) > 16 && !e.IsEncrypted() && bytes.ContainsRune(e, '-')
|
||||
}
|
||||
|
||||
// Parts returns the number of object parts that are
|
||||
|
||||
@@ -78,6 +78,10 @@ var stringTests = []struct {
|
||||
ETag: ETag{144, 64, 44, 120, 210, 220, 205, 222, 225, 233, 232, 98, 34, 206, 44, 99, 97, 103, 95, 53, 41, 210, 96, 0, 174, 46, 144, 15, 242, 22, 179, 203, 89, 225, 48, 224, 146, 216, 162, 152, 30, 119, 111, 77, 11, 214, 9, 65},
|
||||
String: "90402c78d2dccddee1e9e86222ce2c6361675f3529d26000ae2e900ff216b3cb59e130e092d8a2981e776f4d0bd60941",
|
||||
},
|
||||
{ // 5
|
||||
ETag: ETag{32, 0, 15, 0, 219, 45, 144, 167, 180, 7, 130, 212, 207, 242, 180, 26, 119, 153, 252, 30, 126, 173, 37, 151, 45, 182, 81, 80, 17, 141, 251, 226, 186, 118, 163, 192, 2, 218, 40, 248, 92, 132, 12, 210, 0, 26, 40, 169},
|
||||
String: "20000f00db2d90a7b40782d4cff2b41a7799fc1e7ead25972db65150118dfbe2ba76a3c002da28f85c840cd2001a28a9",
|
||||
},
|
||||
}
|
||||
|
||||
func TestString(t *testing.T) {
|
||||
@@ -185,6 +189,30 @@ func TestMultipart(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
var isEncryptedTests = []struct {
|
||||
ETag string
|
||||
IsEncrypted bool
|
||||
}{
|
||||
{ETag: "20000f00db2d90a7b40782d4cff2b41a7799fc1e7ead25972db65150118dfbe2ba76a3c002da28f85c840cd2001a28a9", IsEncrypted: true}, // 0
|
||||
|
||||
{ETag: "3b83ef96387f14655fc854ddc3c6bd57"}, // 1
|
||||
{ETag: "7b976cc68452e003eec7cb0eb631a19a-1"}, // 2
|
||||
{ETag: "a7d414b9133d6483d9a1c4e04e856e3b-2"}, // 3
|
||||
{ETag: "7b976cc68452e003eec7cb0eb631a19a-10000"}, // 4
|
||||
}
|
||||
|
||||
func TestIsEncrypted(t *testing.T) {
|
||||
for i, test := range isEncryptedTests {
|
||||
tag, err := Parse(test.ETag)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: failed to parse ETag: %v", i, err)
|
||||
}
|
||||
if isEncrypted := tag.IsEncrypted(); isEncrypted != test.IsEncrypted {
|
||||
t.Fatalf("Test %d: got '%v' - want '%v'", i, isEncrypted, test.IsEncrypted)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var fromContentMD5Tests = []struct {
|
||||
Header http.Header
|
||||
ETag ETag
|
||||
|
||||
@@ -228,8 +228,6 @@ func (target *WebhookTarget) Send(eventKey string) error {
|
||||
|
||||
// Close - does nothing and available for interface compatibility.
|
||||
func (target *WebhookTarget) Close() error {
|
||||
// Close idle connection with "keep-alive" states
|
||||
target.httpClient.CloseIdleConnections()
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//go:build (linux && arm) || (linux && ppc64) || (linux && ppc64le) || (linux && s390x)
|
||||
// +build linux,arm linux,ppc64 linux,ppc64le linux,s390x
|
||||
//go:build (linux && arm) || (linux && ppc64) || (linux && ppc64le) || (linux && s390x)|| (linux && riscv64)
|
||||
// +build linux,arm linux,ppc64 linux,ppc64le linux,s390x linux,riscv64
|
||||
|
||||
package kernel
|
||||
|
||||
|
||||
+61
-5
@@ -22,11 +22,16 @@ import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/minio/kes"
|
||||
)
|
||||
|
||||
const (
|
||||
tlsClientSessionCacheSize = 100
|
||||
)
|
||||
|
||||
// Config contains various KMS-related configuration
|
||||
// parameters - like KMS endpoints or authentication
|
||||
// credentials.
|
||||
@@ -59,20 +64,36 @@ func NewWithConfig(config Config) (KMS, error) {
|
||||
copy(endpoints, config.Endpoints)
|
||||
|
||||
client := kes.NewClientWithConfig("", &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
Certificates: []tls.Certificate{config.Certificate},
|
||||
RootCAs: config.RootCAs,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
Certificates: []tls.Certificate{config.Certificate},
|
||||
RootCAs: config.RootCAs,
|
||||
ClientSessionCache: tls.NewLRUClientSessionCache(tlsClientSessionCacheSize),
|
||||
})
|
||||
client.Endpoints = endpoints
|
||||
|
||||
var bulkAvailable bool
|
||||
_, policy, err := client.DescribeSelf(context.Background())
|
||||
if err == nil {
|
||||
const BulkAPI = "/v1/key/bulk/decrypt/"
|
||||
for _, allow := range policy.Allow {
|
||||
if strings.HasPrefix(allow, BulkAPI) {
|
||||
bulkAvailable = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return &kesClient{
|
||||
client: client,
|
||||
defaultKeyID: config.DefaultKeyID,
|
||||
client: client,
|
||||
defaultKeyID: config.DefaultKeyID,
|
||||
bulkAvailable: bulkAvailable,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type kesClient struct {
|
||||
defaultKeyID string
|
||||
client *kes.Client
|
||||
|
||||
bulkAvailable bool
|
||||
}
|
||||
|
||||
var _ KMS = (*kesClient)(nil) // compiler check
|
||||
@@ -140,3 +161,38 @@ func (c *kesClient) DecryptKey(keyID string, ciphertext []byte, ctx Context) ([]
|
||||
}
|
||||
return c.client.Decrypt(context.Background(), keyID, ciphertext, ctxBytes)
|
||||
}
|
||||
|
||||
func (c *kesClient) DecryptAll(keyID string, ciphertexts [][]byte, contexts []Context) ([][]byte, error) {
|
||||
if c.bulkAvailable {
|
||||
CCPs := make([]kes.CCP, 0, len(ciphertexts))
|
||||
for i := range ciphertexts {
|
||||
bCtx, err := contexts[i].MarshalText()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
CCPs = append(CCPs, kes.CCP{
|
||||
Ciphertext: ciphertexts[i],
|
||||
Context: bCtx,
|
||||
})
|
||||
}
|
||||
PCPs, err := c.client.DecryptAll(context.Background(), keyID, CCPs...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
plaintexts := make([][]byte, 0, len(PCPs))
|
||||
for _, p := range PCPs {
|
||||
plaintexts = append(plaintexts, p.Plaintext)
|
||||
}
|
||||
return plaintexts, nil
|
||||
}
|
||||
|
||||
plaintexts := make([][]byte, 0, len(ciphertexts))
|
||||
for i := range ciphertexts {
|
||||
plaintext, err := c.DecryptKey(keyID, ciphertexts[i], contexts[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
plaintexts = append(plaintexts, plaintext)
|
||||
}
|
||||
return plaintexts, nil
|
||||
}
|
||||
|
||||
@@ -20,8 +20,10 @@ package kms
|
||||
import (
|
||||
"encoding"
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/minio/pkg/env"
|
||||
)
|
||||
|
||||
// KMS is the generic interface that abstracts over
|
||||
@@ -51,6 +53,23 @@ type KMS interface {
|
||||
// by the key ID. The context must match the context value
|
||||
// used to generate the ciphertext.
|
||||
DecryptKey(keyID string, ciphertext []byte, context Context) ([]byte, error)
|
||||
|
||||
// DecryptAll decrypts all ciphertexts with the key referenced
|
||||
// by the key ID. The contexts must match the context value
|
||||
// used to generate the ciphertexts.
|
||||
DecryptAll(keyID string, ciphertext [][]byte, context []Context) ([][]byte, error)
|
||||
}
|
||||
|
||||
// BatchSize returns the size of the batches that should be used during
|
||||
// KES bulk decryption API calls.
|
||||
func BatchSize() int {
|
||||
const DefaultBatchSize = 500
|
||||
v := env.Get("MINIO_KMS_KES_BULK_API_BATCH_SIZE", strconv.Itoa(DefaultBatchSize))
|
||||
n, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
return DefaultBatchSize
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// Status describes the current state of a KMS.
|
||||
|
||||
@@ -224,6 +224,18 @@ func (kms secretKey) DecryptKey(keyID string, ciphertext []byte, context Context
|
||||
return plaintext, nil
|
||||
}
|
||||
|
||||
func (kms secretKey) DecryptAll(keyID string, ciphertexts [][]byte, contexts []Context) ([][]byte, error) {
|
||||
plaintexts := make([][]byte, 0, len(ciphertexts))
|
||||
for i := range ciphertexts {
|
||||
plaintext, err := kms.DecryptKey(keyID, ciphertexts[i], contexts[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
plaintexts = append(plaintexts, plaintext)
|
||||
}
|
||||
return plaintexts, nil
|
||||
}
|
||||
|
||||
type encryptedKey struct {
|
||||
Algorithm string `json:"aead"`
|
||||
IV []byte `json:"iv"`
|
||||
|
||||
@@ -360,7 +360,10 @@ func logIf(ctx context.Context, err error, errKind ...interface{}) {
|
||||
// Iterate over all logger targets to send the log entry
|
||||
for _, t := range SystemTargets() {
|
||||
if err := t.Send(entry, entry.LogKind); err != nil {
|
||||
LogAlwaysIf(context.Background(), fmt.Errorf("event(%v) was not sent to Logger target (%v): %v", entry, t, err), entry.LogKind)
|
||||
if consoleTgt != nil {
|
||||
entry.Trace.Message = fmt.Sprintf("event(%#v) was not sent to Logger target (%#v): %#v", entry, t, err)
|
||||
consoleTgt.Send(entry, entry.LogKind)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,9 +128,6 @@ func acceptedResponseStatusCode(code int) bool {
|
||||
}
|
||||
|
||||
func (h *Target) logEntry(entry interface{}) {
|
||||
h.wg.Add(1)
|
||||
defer h.wg.Done()
|
||||
|
||||
logJSON, err := json.Marshal(&entry)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -181,6 +178,8 @@ func (h *Target) startHTTPLogger() {
|
||||
// Create a routine which sends json logs received
|
||||
// from an internal channel.
|
||||
go func() {
|
||||
h.wg.Add(1)
|
||||
defer h.wg.Done()
|
||||
for entry := range h.logCh {
|
||||
h.logEntry(entry)
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
sarama "github.com/Shopify/sarama"
|
||||
"github.com/Shopify/sarama"
|
||||
saramatls "github.com/Shopify/sarama/tools/tls"
|
||||
|
||||
"github.com/minio/minio/internal/logger/message/audit"
|
||||
@@ -62,9 +62,6 @@ func (h *Target) Send(entry interface{}, errKind string) error {
|
||||
}
|
||||
|
||||
func (h *Target) logEntry(entry interface{}) {
|
||||
h.wg.Add(1)
|
||||
defer h.wg.Done()
|
||||
|
||||
logJSON, err := json.Marshal(&entry)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -90,6 +87,8 @@ func (h *Target) startKakfaLogger() {
|
||||
// Create a routine which sends json logs received
|
||||
// from an internal channel.
|
||||
go func() {
|
||||
h.wg.Add(1)
|
||||
defer h.wg.Done()
|
||||
for entry := range h.logCh {
|
||||
h.logEntry(entry)
|
||||
}
|
||||
|
||||
@@ -46,7 +46,11 @@ var (
|
||||
// Must be immutable at all times.
|
||||
// Can be swapped to another while holding swapMu
|
||||
systemTargets = []Target{}
|
||||
nTargets int32 // atomic count of len(targets)
|
||||
|
||||
// This is always set represent /dev/console target
|
||||
consoleTgt Target
|
||||
|
||||
nTargets int32 // atomic count of len(targets)
|
||||
)
|
||||
|
||||
// SystemTargets returns active targets.
|
||||
@@ -90,6 +94,11 @@ func AddSystemTarget(t Target) error {
|
||||
return err
|
||||
}
|
||||
swapMu.Lock()
|
||||
if consoleTgt == nil {
|
||||
if t.Type() == types.TargetConsole {
|
||||
consoleTgt = t
|
||||
}
|
||||
}
|
||||
updated := append(make([]Target, 0, len(systemTargets)+1), systemTargets...)
|
||||
updated = append(updated, t)
|
||||
systemTargets = updated
|
||||
|
||||
@@ -101,6 +101,15 @@ func errInvalidRequestParameter(err error) *s3Error {
|
||||
}
|
||||
}
|
||||
|
||||
func errInvalidScanRangeParameter(err error) *s3Error {
|
||||
return &s3Error{
|
||||
code: "InvalidRequestParameter",
|
||||
message: "The value of a parameter in ScanRange element is invalid. Check the service API documentation and try again.",
|
||||
statusCode: 400,
|
||||
cause: err,
|
||||
}
|
||||
}
|
||||
|
||||
func errObjectSerializationConflict(err error) *s3Error {
|
||||
return &s3Error{
|
||||
code: "ObjectSerializationConflict",
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
package json
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
@@ -90,45 +89,31 @@ func NewReader(readCloser io.ReadCloser, args *ReaderArgs) *Reader {
|
||||
}
|
||||
}
|
||||
|
||||
// syncReadCloser will wrap a readcloser and make it safe to call Close
|
||||
// while reads are running.
|
||||
// All read errors are also postponed until Close is called and
|
||||
// io.EOF is returned instead.
|
||||
// syncReadCloser will wrap a readcloser and make it safe to call Close while
|
||||
// reads are running.
|
||||
type syncReadCloser struct {
|
||||
rc io.ReadCloser
|
||||
errMu sync.Mutex
|
||||
err error
|
||||
rc io.ReadCloser
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (pr *syncReadCloser) Read(p []byte) (n int, err error) {
|
||||
// This ensures that Close will block until Read has completed.
|
||||
// This allows another goroutine to close the reader.
|
||||
pr.errMu.Lock()
|
||||
defer pr.errMu.Unlock()
|
||||
if pr.err != nil {
|
||||
pr.mu.Lock()
|
||||
defer pr.mu.Unlock()
|
||||
if pr.rc == nil {
|
||||
return 0, io.EOF
|
||||
}
|
||||
n, pr.err = pr.rc.Read(p)
|
||||
if pr.err != nil {
|
||||
// Translate any error into io.EOF, so we don't crash:
|
||||
// https://github.com/bcicen/jstream/blob/master/scanner.go#L48
|
||||
return n, io.EOF
|
||||
}
|
||||
|
||||
return n, nil
|
||||
return pr.rc.Read(p)
|
||||
}
|
||||
|
||||
var errClosed = errors.New("read after close")
|
||||
|
||||
func (pr *syncReadCloser) Close() error {
|
||||
pr.errMu.Lock()
|
||||
defer pr.errMu.Unlock()
|
||||
if pr.err == errClosed {
|
||||
return nil
|
||||
pr.mu.Lock()
|
||||
defer pr.mu.Unlock()
|
||||
if pr.rc != nil {
|
||||
err := pr.rc.Close()
|
||||
pr.rc = nil
|
||||
return err
|
||||
}
|
||||
if pr.err != nil {
|
||||
return pr.err
|
||||
}
|
||||
pr.err = errClosed
|
||||
return pr.rc.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
+80
-13
@@ -212,6 +212,69 @@ type RequestProgress struct {
|
||||
Enabled bool `xml:"Enabled"`
|
||||
}
|
||||
|
||||
// ScanRange represents the ScanRange parameter.
|
||||
type ScanRange struct {
|
||||
// Start if byte offset form the start off the file.
|
||||
Start *uint64 `xml:"Start"`
|
||||
// End is the last byte that should be returned, if Start is set,
|
||||
// or the offset from EOF to start reading if start is not present.
|
||||
End *uint64 `xml:"End"`
|
||||
}
|
||||
|
||||
// Validate if the scan range is valid.
|
||||
func (s *ScanRange) Validate() error {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
if s.Start == nil && s.End == nil {
|
||||
// This parameter is optional, but when specified, it must not be empty.
|
||||
// Ref: https://docs.aws.amazon.com/AmazonS3/latest/API/API_SelectObjectContent.html#AmazonS3-SelectObjectContent-request-ScanRange
|
||||
return errors.New("ScanRange: No Start or End specified")
|
||||
}
|
||||
if s.Start == nil || s.End == nil {
|
||||
return nil
|
||||
}
|
||||
if *s.Start > *s.End {
|
||||
return errors.New("ScanRange: Start cannot be after end")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartLen returns start offset plus length from range.
|
||||
func (s *ScanRange) StartLen() (start, length int64, err error) {
|
||||
if s == nil {
|
||||
return 0, -1, nil
|
||||
}
|
||||
err = s.Validate()
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
if s.End == nil && s.Start == nil {
|
||||
// Not valid, but should be caught above.
|
||||
return 0, -1, nil
|
||||
}
|
||||
if s.End == nil {
|
||||
start := int64(*s.Start)
|
||||
if start < 0 {
|
||||
return 0, 0, errors.New("ScanRange: Start after EOF")
|
||||
}
|
||||
return start, -1, nil
|
||||
}
|
||||
if s.Start == nil {
|
||||
// Suffix length
|
||||
end := int64(*s.End)
|
||||
if end < 0 {
|
||||
return 0, 0, errors.New("ScanRange: End bigger than file")
|
||||
}
|
||||
// Suffix length
|
||||
return -end, -1, nil
|
||||
}
|
||||
start = int64(*s.Start)
|
||||
end := int64(*s.End)
|
||||
return start, end - start + 1, nil
|
||||
}
|
||||
|
||||
// S3Select - filters the contents on a simple structured query language (SQL) statement. It
|
||||
// represents elements inside <SelectRequest/> in request XML specified in detail at
|
||||
// https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectSELECTContent.html.
|
||||
@@ -222,11 +285,11 @@ type S3Select struct {
|
||||
Input InputSerialization `xml:"InputSerialization"`
|
||||
Output OutputSerialization `xml:"OutputSerialization"`
|
||||
Progress RequestProgress `xml:"RequestProgress"`
|
||||
ScanRange *ScanRange `xml:"ScanRange"`
|
||||
|
||||
statement *sql.SelectStatement
|
||||
progressReader *progressReader
|
||||
recordReader recordReader
|
||||
close func() error
|
||||
}
|
||||
|
||||
var legacyXMLName = "SelectObjectContentRequest"
|
||||
@@ -251,7 +314,9 @@ func (s3Select *S3Select) UnmarshalXML(d *xml.Decoder, start xml.StartElement) e
|
||||
|
||||
return errMalformedXML(err)
|
||||
}
|
||||
|
||||
if err := parsedS3Select.ScanRange.Validate(); err != nil {
|
||||
return errInvalidScanRangeParameter(err)
|
||||
}
|
||||
parsedS3Select.ExpressionType = strings.ToLower(parsedS3Select.ExpressionType)
|
||||
if parsedS3Select.ExpressionType != "sql" {
|
||||
return errInvalidExpressionType(fmt.Errorf("invalid expression type '%v'", parsedS3Select.ExpressionType))
|
||||
@@ -298,9 +363,13 @@ func (s3Select *S3Select) getProgress() (bytesScanned, bytesProcessed int64) {
|
||||
// Open - opens S3 object by using callback for SQL selection query.
|
||||
// Currently CSV, JSON and Apache Parquet formats are supported.
|
||||
func (s3Select *S3Select) Open(getReader func(offset, length int64) (io.ReadCloser, error)) error {
|
||||
offset, end, err := s3Select.ScanRange.StartLen()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch s3Select.Input.format {
|
||||
case csvFormat:
|
||||
rc, err := getReader(0, -1)
|
||||
rc, err := getReader(offset, end)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -313,7 +382,9 @@ func (s3Select *S3Select) Open(getReader func(offset, length int64) (io.ReadClos
|
||||
|
||||
s3Select.recordReader, err = csv.NewReader(s3Select.progressReader, &s3Select.Input.CSVArgs)
|
||||
if err != nil {
|
||||
rc.Close()
|
||||
// Close all reader resources opened so far.
|
||||
s3Select.progressReader.Close()
|
||||
|
||||
var stErr bzip2.StructuralError
|
||||
if errors.As(err, &stErr) {
|
||||
return errInvalidCompression(err, s3Select.Input.CompressionType)
|
||||
@@ -332,10 +403,9 @@ func (s3Select *S3Select) Open(getReader func(offset, length int64) (io.ReadClos
|
||||
}
|
||||
return err
|
||||
}
|
||||
s3Select.close = rc.Close
|
||||
return nil
|
||||
case jsonFormat:
|
||||
rc, err := getReader(0, -1)
|
||||
rc, err := getReader(offset, end)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -356,12 +426,15 @@ func (s3Select *S3Select) Open(getReader func(offset, length int64) (io.ReadClos
|
||||
s3Select.recordReader = json.NewReader(s3Select.progressReader, &s3Select.Input.JSONArgs)
|
||||
}
|
||||
|
||||
s3Select.close = rc.Close
|
||||
return nil
|
||||
case parquetFormat:
|
||||
if !strings.EqualFold(os.Getenv("MINIO_API_SELECT_PARQUET"), "on") {
|
||||
return errors.New("parquet format parsing not enabled on server")
|
||||
}
|
||||
if offset != 0 || end != -1 {
|
||||
// Offsets do not make sense in parquet files.
|
||||
return errors.New("parquet format does not support offsets")
|
||||
}
|
||||
var err error
|
||||
s3Select.recordReader, err = parquet.NewReader(getReader, &s3Select.Input.ParquetArgs)
|
||||
return err
|
||||
@@ -420,12 +493,6 @@ func (s3Select *S3Select) marshal(buf *bytes.Buffer, record sql.Record) error {
|
||||
|
||||
// Evaluate - filters and sends records read from opened reader as per select statement to http response writer.
|
||||
func (s3Select *S3Select) Evaluate(w http.ResponseWriter) {
|
||||
defer func() {
|
||||
if s3Select.close != nil {
|
||||
s3Select.close()
|
||||
}
|
||||
}()
|
||||
|
||||
getProgressFunc := s3Select.getProgress
|
||||
if !s3Select.Progress.Enabled {
|
||||
getProgressFunc = nil
|
||||
|
||||
@@ -20,6 +20,7 @@ package s3select
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
@@ -1374,6 +1375,314 @@ func TestJSONInput(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCSVRanges(t *testing.T) {
|
||||
testInput := []byte(`id,time,num,num2,text
|
||||
1,2010-01-01T,7867786,4565.908123,"a text, with comma"
|
||||
2,2017-01-02T03:04Z,-5, 0.765111,
|
||||
`)
|
||||
testTable := []struct {
|
||||
name string
|
||||
query string
|
||||
input []byte
|
||||
requestXML []byte // override request XML
|
||||
wantResult string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "select-all",
|
||||
input: testInput,
|
||||
query: ``,
|
||||
// Since we are doing offset, no headers are used.
|
||||
wantResult: `{"_1":"2","_2":"2017-01-02T03:04Z","_3":"-5","_4":" 0.765111","_5":""}`,
|
||||
requestXML: []byte(`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<SelectObjectContentRequest>
|
||||
<Expression>SELECT * from s3object AS s</Expression>
|
||||
<ExpressionType>SQL</ExpressionType>
|
||||
<InputSerialization>
|
||||
<CompressionType>NONE</CompressionType>
|
||||
<CSV>
|
||||
<FileHeaderInfo>NONE</FileHeaderInfo>
|
||||
<QuoteCharacter>"</QuoteCharacter>
|
||||
</CSV>
|
||||
</InputSerialization>
|
||||
<OutputSerialization>
|
||||
<JSON>
|
||||
</JSON>
|
||||
</OutputSerialization>
|
||||
<RequestProgress>
|
||||
<Enabled>FALSE</Enabled>
|
||||
</RequestProgress>
|
||||
<ScanRange><Start>76</Start><End>109</End></ScanRange>
|
||||
</SelectObjectContentRequest>`),
|
||||
},
|
||||
{
|
||||
name: "select-remain",
|
||||
input: testInput,
|
||||
// Since we are doing offset, no headers are used.
|
||||
wantResult: `{"_1":"2","_2":"2017-01-02T03:04Z","_3":"-5","_4":" 0.765111","_5":""}`,
|
||||
requestXML: []byte(`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<SelectObjectContentRequest>
|
||||
<Expression>SELECT * from s3object AS s</Expression>
|
||||
<ExpressionType>SQL</ExpressionType>
|
||||
<InputSerialization>
|
||||
<CompressionType>NONE</CompressionType>
|
||||
<CSV>
|
||||
<FileHeaderInfo>NONE</FileHeaderInfo>
|
||||
<QuoteCharacter>"</QuoteCharacter>
|
||||
</CSV>
|
||||
</InputSerialization>
|
||||
<OutputSerialization>
|
||||
<JSON>
|
||||
</JSON>
|
||||
</OutputSerialization>
|
||||
<RequestProgress>
|
||||
<Enabled>FALSE</Enabled>
|
||||
</RequestProgress>
|
||||
<ScanRange><Start>76</Start></ScanRange>
|
||||
</SelectObjectContentRequest>`),
|
||||
},
|
||||
{
|
||||
name: "select-end-bytes",
|
||||
input: testInput,
|
||||
query: ``,
|
||||
// Since we are doing offset, no headers are used.
|
||||
wantResult: `{"_1":"2","_2":"2017-01-02T03:04Z","_3":"-5","_4":" 0.765111","_5":""}`,
|
||||
requestXML: []byte(`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<SelectObjectContentRequest>
|
||||
<Expression>SELECT * from s3object AS s</Expression>
|
||||
<ExpressionType>SQL</ExpressionType>
|
||||
<InputSerialization>
|
||||
<CompressionType>NONE</CompressionType>
|
||||
<CSV>
|
||||
<FileHeaderInfo>NONE</FileHeaderInfo>
|
||||
<QuoteCharacter>"</QuoteCharacter>
|
||||
</CSV>
|
||||
</InputSerialization>
|
||||
<OutputSerialization>
|
||||
<JSON>
|
||||
</JSON>
|
||||
</OutputSerialization>
|
||||
<RequestProgress>
|
||||
<Enabled>FALSE</Enabled>
|
||||
</RequestProgress>
|
||||
<ScanRange><End>35</End></ScanRange>
|
||||
</SelectObjectContentRequest>`),
|
||||
},
|
||||
{
|
||||
name: "select-middle",
|
||||
input: testInput,
|
||||
// Since we are doing offset, no headers are used.
|
||||
wantResult: `{"_1":"a text, with comma"}`,
|
||||
requestXML: []byte(`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<SelectObjectContentRequest>
|
||||
<Expression>SELECT * from s3object AS s</Expression>
|
||||
<ExpressionType>SQL</ExpressionType>
|
||||
<InputSerialization>
|
||||
<CompressionType>NONE</CompressionType>
|
||||
<CSV>
|
||||
<FileHeaderInfo>NONE</FileHeaderInfo>
|
||||
<QuoteCharacter>"</QuoteCharacter>
|
||||
</CSV>
|
||||
</InputSerialization>
|
||||
<OutputSerialization>
|
||||
<JSON>
|
||||
</JSON>
|
||||
</OutputSerialization>
|
||||
<RequestProgress>
|
||||
<Enabled>FALSE</Enabled>
|
||||
</RequestProgress>
|
||||
<ScanRange><Start>56</Start><End>76</End></ScanRange>
|
||||
</SelectObjectContentRequest>`),
|
||||
},
|
||||
{
|
||||
name: "error-end-before-start",
|
||||
input: testInput,
|
||||
// Since we are doing offset, no headers are used.
|
||||
wantResult: ``,
|
||||
wantErr: true,
|
||||
requestXML: []byte(`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<SelectObjectContentRequest>
|
||||
<Expression>SELECT * from s3object AS s</Expression>
|
||||
<ExpressionType>SQL</ExpressionType>
|
||||
<InputSerialization>
|
||||
<CompressionType>NONE</CompressionType>
|
||||
<CSV>
|
||||
<FileHeaderInfo>NONE</FileHeaderInfo>
|
||||
<QuoteCharacter>"</QuoteCharacter>
|
||||
</CSV>
|
||||
</InputSerialization>
|
||||
<OutputSerialization>
|
||||
<JSON>
|
||||
</JSON>
|
||||
</OutputSerialization>
|
||||
<RequestProgress>
|
||||
<Enabled>FALSE</Enabled>
|
||||
</RequestProgress>
|
||||
<ScanRange><Start>56</Start><End>26</End></ScanRange>
|
||||
</SelectObjectContentRequest>`),
|
||||
},
|
||||
{
|
||||
name: "error-empty",
|
||||
input: testInput,
|
||||
// Since we are doing offset, no headers are used.
|
||||
wantResult: ``,
|
||||
wantErr: true,
|
||||
requestXML: []byte(`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<SelectObjectContentRequest>
|
||||
<Expression>SELECT * from s3object AS s</Expression>
|
||||
<ExpressionType>SQL</ExpressionType>
|
||||
<InputSerialization>
|
||||
<CompressionType>NONE</CompressionType>
|
||||
<CSV>
|
||||
<FileHeaderInfo>NONE</FileHeaderInfo>
|
||||
<QuoteCharacter>"</QuoteCharacter>
|
||||
</CSV>
|
||||
</InputSerialization>
|
||||
<OutputSerialization>
|
||||
<JSON>
|
||||
</JSON>
|
||||
</OutputSerialization>
|
||||
<RequestProgress>
|
||||
<Enabled>FALSE</Enabled>
|
||||
</RequestProgress>
|
||||
<ScanRange></ScanRange>
|
||||
</SelectObjectContentRequest>`),
|
||||
},
|
||||
{
|
||||
name: "error-after-eof",
|
||||
input: testInput,
|
||||
// Since we are doing offset, no headers are used.
|
||||
wantResult: ``,
|
||||
wantErr: true,
|
||||
requestXML: []byte(`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<SelectObjectContentRequest>
|
||||
<Expression>SELECT * from s3object AS s</Expression>
|
||||
<ExpressionType>SQL</ExpressionType>
|
||||
<InputSerialization>
|
||||
<CompressionType>NONE</CompressionType>
|
||||
<CSV>
|
||||
<FileHeaderInfo>NONE</FileHeaderInfo>
|
||||
<QuoteCharacter>"</QuoteCharacter>
|
||||
</CSV>
|
||||
</InputSerialization>
|
||||
<OutputSerialization>
|
||||
<JSON>
|
||||
</JSON>
|
||||
</OutputSerialization>
|
||||
<RequestProgress>
|
||||
<Enabled>FALSE</Enabled>
|
||||
</RequestProgress>
|
||||
<ScanRange><Start>2600000</Start></ScanRange>
|
||||
</SelectObjectContentRequest>`),
|
||||
},
|
||||
{
|
||||
name: "error-after-eof",
|
||||
input: testInput,
|
||||
// Since we are doing offset, no headers are used.
|
||||
wantResult: ``,
|
||||
wantErr: true,
|
||||
requestXML: []byte(`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<SelectObjectContentRequest>
|
||||
<Expression>SELECT * from s3object AS s</Expression>
|
||||
<ExpressionType>SQL</ExpressionType>
|
||||
<InputSerialization>
|
||||
<CompressionType>NONE</CompressionType>
|
||||
<CSV>
|
||||
<FileHeaderInfo>NONE</FileHeaderInfo>
|
||||
<QuoteCharacter>"</QuoteCharacter>
|
||||
</CSV>
|
||||
</InputSerialization>
|
||||
<OutputSerialization>
|
||||
<JSON>
|
||||
</JSON>
|
||||
</OutputSerialization>
|
||||
<RequestProgress>
|
||||
<Enabled>FALSE</Enabled>
|
||||
</RequestProgress>
|
||||
<ScanRange><Start>2600000</Start><End>2600001</End></ScanRange>
|
||||
</SelectObjectContentRequest>`),
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testTable {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
testReq := testCase.requestXML
|
||||
s3Select, err := NewS3Select(bytes.NewReader(testReq))
|
||||
if err != nil {
|
||||
if !testCase.wantErr {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("got expected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = s3Select.Open(func(offset, length int64) (io.ReadCloser, error) {
|
||||
in := testCase.input
|
||||
if offset != 0 || length != -1 {
|
||||
// Copy from SelectObjectContentHandler
|
||||
isSuffixLength := false
|
||||
if offset < 0 {
|
||||
isSuffixLength = true
|
||||
}
|
||||
|
||||
if length > 0 {
|
||||
length--
|
||||
}
|
||||
|
||||
rs := &httpRangeSpec{
|
||||
IsSuffixLength: isSuffixLength,
|
||||
Start: offset,
|
||||
End: offset + length,
|
||||
}
|
||||
if length == -1 {
|
||||
rs.End = -1
|
||||
}
|
||||
t.Log("input, offset:", offset, "length:", length, "size:", len(in))
|
||||
offset, length, err = rs.GetOffsetLength(int64(len(in)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t.Log("rs:", *rs, "offset:", offset, "length:", length)
|
||||
in = in[offset : offset+length]
|
||||
}
|
||||
return ioutil.NopCloser(bytes.NewBuffer(in)), nil
|
||||
}); err != nil {
|
||||
if !testCase.wantErr {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("got expected error: %v", err)
|
||||
return
|
||||
} else if testCase.wantErr {
|
||||
t.Error("did not get expected error")
|
||||
return
|
||||
}
|
||||
|
||||
w := &testResponseWriter{}
|
||||
s3Select.Evaluate(w)
|
||||
s3Select.Close()
|
||||
resp := http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: ioutil.NopCloser(bytes.NewReader(w.response)),
|
||||
ContentLength: int64(len(w.response)),
|
||||
}
|
||||
res, err := minio.NewSelectResults(&resp, "testbucket")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
got, err := ioutil.ReadAll(res)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
gotS := strings.TrimSpace(string(got))
|
||||
if !reflect.DeepEqual(gotS, testCase.wantResult) {
|
||||
t.Errorf("received response does not match with expected reply. Query: %s\ngot: %s\nwant:%s", testCase.query, gotS, testCase.wantResult)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParquetInput(t *testing.T) {
|
||||
os.Setenv("MINIO_API_SELECT_PARQUET", "on")
|
||||
defer os.Setenv("MINIO_API_SELECT_PARQUET", "off")
|
||||
@@ -1728,3 +2037,76 @@ func TestParquetInputSchemaCSV(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// httpRangeSpec represents a range specification as supported by S3 GET
|
||||
// object request.
|
||||
//
|
||||
// Case 1: Not present -> represented by a nil RangeSpec
|
||||
// Case 2: bytes=1-10 (absolute start and end offsets) -> RangeSpec{false, 1, 10}
|
||||
// Case 3: bytes=10- (absolute start offset with end offset unspecified) -> RangeSpec{false, 10, -1}
|
||||
// Case 4: bytes=-30 (suffix length specification) -> RangeSpec{true, -30, -1}
|
||||
type httpRangeSpec struct {
|
||||
// Does the range spec refer to a suffix of the object?
|
||||
IsSuffixLength bool
|
||||
|
||||
// Start and end offset specified in range spec
|
||||
Start, End int64
|
||||
}
|
||||
|
||||
func (h *httpRangeSpec) GetLength(resourceSize int64) (rangeLength int64, err error) {
|
||||
switch {
|
||||
case resourceSize < 0:
|
||||
return 0, errors.New("Resource size cannot be negative")
|
||||
|
||||
case h == nil:
|
||||
rangeLength = resourceSize
|
||||
|
||||
case h.IsSuffixLength:
|
||||
specifiedLen := -h.Start
|
||||
rangeLength = specifiedLen
|
||||
if specifiedLen > resourceSize {
|
||||
rangeLength = resourceSize
|
||||
}
|
||||
|
||||
case h.Start >= resourceSize:
|
||||
return 0, errors.New("errInvalidRange")
|
||||
|
||||
case h.End > -1:
|
||||
end := h.End
|
||||
if resourceSize <= end {
|
||||
end = resourceSize - 1
|
||||
}
|
||||
rangeLength = end - h.Start + 1
|
||||
|
||||
case h.End == -1:
|
||||
rangeLength = resourceSize - h.Start
|
||||
|
||||
default:
|
||||
return 0, errors.New("Unexpected range specification case")
|
||||
}
|
||||
|
||||
return rangeLength, nil
|
||||
}
|
||||
|
||||
// GetOffsetLength computes the start offset and length of the range
|
||||
// given the size of the resource
|
||||
func (h *httpRangeSpec) GetOffsetLength(resourceSize int64) (start, length int64, err error) {
|
||||
if h == nil {
|
||||
// No range specified, implies whole object.
|
||||
return 0, resourceSize, nil
|
||||
}
|
||||
|
||||
length, err = h.GetLength(resourceSize)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
start = h.Start
|
||||
if h.IsSuffixLength {
|
||||
start = resourceSize + h.Start
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
}
|
||||
return start, length, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user