diff --git a/cmd/admin-handlers-users.go b/cmd/admin-handlers-users.go index 0e54c13deac39..641d581953b3d 100644 --- a/cmd/admin-handlers-users.go +++ b/cmd/admin-handlers-users.go @@ -37,6 +37,7 @@ import ( "github.com/minio/madmin-go/v3" "github.com/minio/minio/internal/auth" "github.com/minio/minio/internal/config/dns" + "github.com/minio/minio/internal/logger" "github.com/minio/mux" xldap "github.com/minio/pkg/v3/ldap" "github.com/minio/pkg/v3/policy" @@ -1579,6 +1580,7 @@ func (a adminAPIHandlers) InfoCannedPolicy(w http.ResponseWriter, r *http.Reques writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errTooManyPolicies), r.URL) return } + setReqInfoPolicyName(ctx, name) policyDoc, err := globalIAMSys.InfoPolicy(name) if err != nil { @@ -1682,6 +1684,7 @@ func (a adminAPIHandlers) RemoveCannedPolicy(w http.ResponseWriter, r *http.Requ vars := mux.Vars(r) policyName := vars["name"] + setReqInfoPolicyName(ctx, policyName) if err := globalIAMSys.DeletePolicy(ctx, policyName, true); err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) @@ -1714,6 +1717,7 @@ func (a adminAPIHandlers) AddCannedPolicy(w http.ResponseWriter, r *http.Request writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminResourceInvalidArgument), r.URL) return } + setReqInfoPolicyName(ctx, policyName) // Error out if Content-Length is missing. if r.ContentLength <= 0 { @@ -1779,6 +1783,7 @@ func (a adminAPIHandlers) SetPolicyForUserOrGroup(w http.ResponseWriter, r *http policyName := vars["policyName"] entityName := vars["userOrGroup"] isGroup := vars["isGroup"] == "true" + setReqInfoPolicyName(ctx, policyName) if !isGroup { ok, _, err := globalIAMSys.IsTempUser(entityName) @@ -1864,7 +1869,7 @@ func (a adminAPIHandlers) SetPolicyForUserOrGroup(w http.ResponseWriter, r *http })) } -// ListPolicyMappingEntities - GET /minio/admin/v3/idp/builtin/polciy-entities?policy=xxx&user=xxx&group=xxx +// ListPolicyMappingEntities - GET /minio/admin/v3/idp/builtin/policy-entities?policy=xxx&user=xxx&group=xxx func (a adminAPIHandlers) ListPolicyMappingEntities(w http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -1966,6 +1971,7 @@ func (a adminAPIHandlers) AttachDetachPolicyBuiltin(w http.ResponseWriter, r *ht writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } + setReqInfoPolicyName(ctx, strings.Join(addedOrRemoved, ",")) respBody := madmin.PolicyAssociationResp{ UpdatedAt: updatedAt, @@ -2812,3 +2818,10 @@ func commonAddServiceAccount(r *http.Request, ldap bool) (context.Context, auth. return ctx, cred, opts, createReq, targetUser, APIError{} } + +// setReqInfoPolicyName will set the given policyName as a tag on the context's request info, +// so that it appears in audit logs. +func setReqInfoPolicyName(ctx context.Context, policyName string) { + reqInfo := logger.GetReqInfo(ctx) + reqInfo.SetTags("policyName", policyName) +} diff --git a/cmd/bucket-handlers.go b/cmd/bucket-handlers.go index 41a57e025fd4c..6db63a042b6f2 100644 --- a/cmd/bucket-handlers.go +++ b/cmd/bucket-handlers.go @@ -1809,6 +1809,10 @@ func (api objectAPIHandlers) PutBucketObjectLockConfigHandler(w http.ResponseWri return } + // Audit log tags. + reqInfo := logger.GetReqInfo(ctx) + reqInfo.SetTags("retention", config.String()) + configData, err := xml.Marshal(config) if err != nil { writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) diff --git a/cmd/bucket-replication.go b/cmd/bucket-replication.go index 1a4fc49c16448..cc90fc8a213d8 100644 --- a/cmd/bucket-replication.go +++ b/cmd/bucket-replication.go @@ -53,8 +53,6 @@ import ( "github.com/minio/minio/internal/once" "github.com/tinylib/msgp/msgp" "github.com/zeebo/xxh3" - "golang.org/x/exp/maps" - "golang.org/x/exp/slices" ) const ( @@ -780,8 +778,9 @@ func putReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo, part isSSEC := crypto.SSEC.IsEncrypted(objInfo.UserDefined) for k, v := range objInfo.UserDefined { + _, isValidSSEHeader := validSSEReplicationHeaders[k] // In case of SSE-C objects copy the allowed internal headers as well - if !isSSEC || !slices.Contains(maps.Keys(validSSEReplicationHeaders), k) { + if !isSSEC || !isValidSSEHeader { if stringsHasPrefixFold(k, ReservedMetadataPrefixLower) { continue } @@ -789,7 +788,7 @@ func putReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo, part continue } } - if slices.Contains(maps.Keys(validSSEReplicationHeaders), k) { + if isValidSSEHeader { meta[validSSEReplicationHeaders[k]] = v } else { meta[k] = v diff --git a/cmd/endpoint.go b/cmd/endpoint.go index d716050bd1fb0..48d1ce9c6178f 100644 --- a/cmd/endpoint.go +++ b/cmd/endpoint.go @@ -26,6 +26,7 @@ import ( "path/filepath" "reflect" "runtime" + "slices" "sort" "strconv" "strings" @@ -38,7 +39,6 @@ import ( "github.com/minio/minio/internal/mountinfo" "github.com/minio/pkg/v3/env" xnet "github.com/minio/pkg/v3/net" - "golang.org/x/exp/slices" ) // EndpointType - enum for endpoint type. diff --git a/cmd/erasure-healing.go b/cmd/erasure-healing.go index bd6abf137195f..4b896e76af154 100644 --- a/cmd/erasure-healing.go +++ b/cmd/erasure-healing.go @@ -23,6 +23,7 @@ import ( "errors" "fmt" "io" + "slices" "strconv" "strings" "sync" @@ -32,7 +33,6 @@ import ( "github.com/minio/minio/internal/grid" "github.com/minio/minio/internal/logger" "github.com/minio/pkg/v3/sync/errgroup" - "golang.org/x/exp/slices" ) //go:generate stringer -type=healingMetric -trimprefix=healingMetric $GOFILE diff --git a/cmd/generic-handlers.go b/cmd/generic-handlers.go index 9298ee40521eb..67b906391eee0 100644 --- a/cmd/generic-handlers.go +++ b/cmd/generic-handlers.go @@ -33,8 +33,6 @@ import ( "github.com/minio/minio-go/v7/pkg/set" "github.com/minio/minio/internal/grid" xnet "github.com/minio/pkg/v3/net" - "golang.org/x/exp/maps" - "golang.org/x/exp/slices" "github.com/minio/minio/internal/amztime" "github.com/minio/minio/internal/config/dns" @@ -75,7 +73,7 @@ const ( // and must not set by clients func containsReservedMetadata(header http.Header) bool { for key := range header { - if slices.Contains(maps.Keys(validSSEReplicationHeaders), key) { + if _, ok := validSSEReplicationHeaders[key]; ok { return false } if stringsHasPrefixFold(key, ReservedMetadataPrefix) { diff --git a/cmd/metrics-v3-types.go b/cmd/metrics-v3-types.go index 859824ef82557..0aa04efa67bac 100644 --- a/cmd/metrics-v3-types.go +++ b/cmd/metrics-v3-types.go @@ -20,6 +20,7 @@ package cmd import ( "context" "fmt" + "slices" "strings" "sync" @@ -27,7 +28,6 @@ import ( "github.com/minio/minio/internal/logger" "github.com/pkg/errors" "github.com/prometheus/client_golang/prometheus" - "golang.org/x/exp/slices" ) type collectorPath string diff --git a/cmd/object-api-utils.go b/cmd/object-api-utils.go index 15aa1f00ea324..45e29c00fbf29 100644 --- a/cmd/object-api-utils.go +++ b/cmd/object-api-utils.go @@ -29,6 +29,7 @@ import ( "net/http" "path" "runtime" + "slices" "strconv" "strings" "sync" @@ -52,7 +53,6 @@ import ( "github.com/minio/pkg/v3/trie" "github.com/minio/pkg/v3/wildcard" "github.com/valyala/bytebufferpool" - "golang.org/x/exp/slices" ) const ( diff --git a/cmd/object-handlers.go b/cmd/object-handlers.go index cf0377fa7372c..b99fb7e0099f1 100644 --- a/cmd/object-handlers.go +++ b/cmd/object-handlers.go @@ -2899,6 +2899,8 @@ func (api objectAPIHandlers) PutObjectRetentionHandler(w http.ResponseWriter, r writeErrorResponse(ctx, w, apiErr, r.URL) return } + reqInfo := logger.GetReqInfo(ctx) + reqInfo.SetTags("retention", objRetention.String()) opts, err := getOpts(ctx, r, bucket, object) if err != nil { diff --git a/cmd/peer-s3-client.go b/cmd/peer-s3-client.go index 16395cf5d5cfd..a28dd1303ae49 100644 --- a/cmd/peer-s3-client.go +++ b/cmd/peer-s3-client.go @@ -21,6 +21,7 @@ import ( "context" "errors" "fmt" + "slices" "sort" "strconv" "sync/atomic" @@ -29,7 +30,6 @@ import ( "github.com/minio/madmin-go/v3" "github.com/minio/minio/internal/grid" "github.com/minio/pkg/v3/sync/errgroup" - "golang.org/x/exp/slices" ) var errPeerOffline = errors.New("peer is offline") diff --git a/cmd/server-main.go b/cmd/server-main.go index c2ce862655cb3..455853282861f 100644 --- a/cmd/server-main.go +++ b/cmd/server-main.go @@ -31,6 +31,7 @@ import ( "os/signal" "path/filepath" "runtime" + "slices" "strings" "syscall" "time" @@ -53,7 +54,6 @@ import ( "github.com/minio/minio/internal/logger" "github.com/minio/pkg/v3/certs" "github.com/minio/pkg/v3/env" - "golang.org/x/exp/slices" "gopkg.in/yaml.v2" ) diff --git a/cmd/signature-v4-utils.go b/cmd/signature-v4-utils.go index 5fa823aec58dd..1569dec1c772e 100644 --- a/cmd/signature-v4-utils.go +++ b/cmd/signature-v4-utils.go @@ -23,6 +23,7 @@ import ( "encoding/hex" "io" "net/http" + "slices" "strconv" "strings" @@ -31,7 +32,6 @@ import ( xhttp "github.com/minio/minio/internal/http" "github.com/minio/minio/internal/logger" "github.com/minio/pkg/v3/policy" - "golang.org/x/exp/slices" ) // http Header "x-amz-content-sha256" == "UNSIGNED-PAYLOAD" indicates that the diff --git a/cmd/sts-handlers_test.go b/cmd/sts-handlers_test.go index 9e743ed50352b..0de49d49c1a6a 100644 --- a/cmd/sts-handlers_test.go +++ b/cmd/sts-handlers_test.go @@ -24,6 +24,7 @@ import ( "io" "os" "reflect" + "slices" "strings" "testing" "time" @@ -34,7 +35,6 @@ import ( cr "github.com/minio/minio-go/v7/pkg/credentials" "github.com/minio/minio-go/v7/pkg/set" "github.com/minio/pkg/v3/ldap" - "golang.org/x/exp/slices" ) func runAllIAMSTSTests(suite *TestSuiteIAM, c *check) { diff --git a/go.mod b/go.mod index 8215c97e27b75..d6c2b2465d660 100644 --- a/go.mod +++ b/go.mod @@ -89,7 +89,6 @@ require ( go.uber.org/zap v1.27.0 goftp.io/server/v2 v2.0.1 golang.org/x/crypto v0.29.0 - golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f golang.org/x/oauth2 v0.24.0 golang.org/x/sync v0.9.0 golang.org/x/sys v0.27.0 diff --git a/go.sum b/go.sum index ee4d48c404f75..3f21c27761758 100644 --- a/go.sum +++ b/go.sum @@ -719,8 +719,6 @@ golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOM golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo= -golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= diff --git a/internal/bucket/lifecycle/rule.go b/internal/bucket/lifecycle/rule.go index 67e026b1bd24c..2fb006066ca05 100644 --- a/internal/bucket/lifecycle/rule.go +++ b/internal/bucket/lifecycle/rule.go @@ -84,10 +84,21 @@ func (r Rule) validateNoncurrentExpiration() error { } func (r Rule) validatePrefixAndFilter() error { - if !r.Prefix.set && r.Filter.IsEmpty() || r.Prefix.set && !r.Filter.IsEmpty() { + // In the now deprecated PutBucketLifecycle API, Rule had a mandatory Prefix element and there existed no Filter field. + // See https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketLifecycle.html + // In the newer PutBucketLifecycleConfiguration API, Rule has a prefix field that is deprecated, and there exists an optional + // Filter field, and within it, an optional Prefix field. + // See https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketLifecycleConfiguration.html + // A valid rule could be a pre-existing one created using the now deprecated PutBucketLifecycle. + // Or, a valid rule could also be either a pre-existing or a new rule that is created using PutBucketLifecycleConfiguration. + // Prefix validation below may check that either Rule.Prefix or Rule.Filter.Prefix exist but not both. + // Here, we assume the pre-existing rule created using PutBucketLifecycle API is already valid and won't fail the validation if Rule.Prefix is empty. + + if r.Prefix.set && !r.Filter.IsEmpty() && r.Filter.Prefix.set { return errXMLNotWellFormed } - if !r.Prefix.set { + + if r.Filter.set { return r.Filter.Validate() } return nil diff --git a/internal/bucket/object/lock/lock.go b/internal/bucket/object/lock/lock.go index 572e9db66d507..79f1421f5356b 100644 --- a/internal/bucket/object/lock/lock.go +++ b/internal/bucket/object/lock/lock.go @@ -237,6 +237,25 @@ type Config struct { } `xml:"Rule,omitempty"` } +// String returns the human readable format of object lock configuration, used in audit logs. +func (config Config) String() string { + parts := []string{ + fmt.Sprintf("Enabled: %v", config.Enabled()), + } + if config.Rule != nil { + if config.Rule.DefaultRetention.Mode != "" { + parts = append(parts, fmt.Sprintf("Mode: %s", config.Rule.DefaultRetention.Mode)) + } + if config.Rule.DefaultRetention.Days != nil { + parts = append(parts, fmt.Sprintf("Days: %d", *config.Rule.DefaultRetention.Days)) + } + if config.Rule.DefaultRetention.Years != nil { + parts = append(parts, fmt.Sprintf("Years: %d", *config.Rule.DefaultRetention.Years)) + } + } + return strings.Join(parts, ", ") +} + // Enabled returns true if config.ObjectLockEnabled is set to Enabled func (config *Config) Enabled() bool { return config.ObjectLockEnabled == Enabled @@ -349,6 +368,10 @@ type ObjectRetention struct { RetainUntilDate RetentionDate `xml:"RetainUntilDate,omitempty"` } +func (o ObjectRetention) String() string { + return fmt.Sprintf("Mode: %s, RetainUntilDate: %s", o.Mode, o.RetainUntilDate.Time) +} + // Maximum 4KiB size per object retention config. const maxObjectRetentionSize = 1 << 12 diff --git a/internal/bucket/object/lock/lock_test.go b/internal/bucket/object/lock/lock_test.go index 91313482ddcae..e31586a76f0b5 100644 --- a/internal/bucket/object/lock/lock_test.go +++ b/internal/bucket/object/lock/lock_test.go @@ -611,3 +611,72 @@ func TestFilterObjectLockMetadata(t *testing.T) { } } } + +func TestToString(t *testing.T) { + days := uint64(30) + daysPtr := &days + years := uint64(2) + yearsPtr := &years + + tests := []struct { + name string + c Config + want string + }{ + { + name: "happy case", + c: Config{ + ObjectLockEnabled: "Enabled", + }, + want: "Enabled: true", + }, + { + name: "with default retention days", + c: Config{ + ObjectLockEnabled: "Enabled", + Rule: &struct { + DefaultRetention DefaultRetention `xml:"DefaultRetention"` + }{ + DefaultRetention: DefaultRetention{ + Mode: RetGovernance, + Days: daysPtr, + }, + }, + }, + want: "Enabled: true, Mode: GOVERNANCE, Days: 30", + }, + { + name: "with default retention years", + c: Config{ + ObjectLockEnabled: "Enabled", + Rule: &struct { + DefaultRetention DefaultRetention `xml:"DefaultRetention"` + }{ + DefaultRetention: DefaultRetention{ + Mode: RetCompliance, + Years: yearsPtr, + }, + }, + }, + want: "Enabled: true, Mode: COMPLIANCE, Years: 2", + }, + { + name: "disabled case", + c: Config{ + ObjectLockEnabled: "Disabled", + }, + want: "Enabled: false", + }, + { + name: "empty case", + c: Config{}, + want: "Enabled: false", + }, + } + for _, tt := range tests { + got := tt.c.String() + if got != tt.want { + t.Errorf("test: %s, got: '%v', want: '%v'", tt.name, got, tt.want) + } + } +}