mirror of
https://github.com/versity/versitygw.git
synced 2026-07-02 16:54:25 +00:00
cd0b4e6d9d
Object key validation allowed internal parent-directory segments such as `public/../private.txt`. Bucket policy and auth checks evaluated the raw key, so a policy allowing bucket/public/* could match the request while posix backend later resolved the key with `filepath.Join` and accessed `bucket/private.txt`. Add backend-specific object key normalization to close that mismatch. The Backend interface now exposes `NormalizeObjectKey` so authorization can evaluate resources using the same key shape a backend will use for storage access. Backends that do not collapse object paths, including Azure and the S3 proxy, inherit `BackendUnsupported.NormalizeObjectKey`. That implementation returns the input key unchanged, avoiding unnecessary normalization and keeping policy evaluation unpolluted for object stores where ../ is part of the key name. posix/scoutfs normalize keys with filepath.Join so policy resources and request keys are compared after internal dot segments are collapsed. Bucket policy evaluation now normalizes both the incoming object key and object resource patterns from the policy before matching. Object lock governance bypass policy checks use the same backend normalizer as well, so retention and legal hold authorization cannot diverge from backend path resolution.
381 lines
12 KiB
Go
381 lines
12 KiB
Go
// Copyright 2023 Versity Software
|
|
// This file is licensed under the Apache License, Version 2.0
|
|
// (the "License"); you may not use this file except in compliance
|
|
// with the License. You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing,
|
|
// software distributed under the License is distributed on an
|
|
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
// KIND, either express or implied. See the License for the
|
|
// specific language governing permissions and limitations
|
|
// under the License.
|
|
|
|
package auth
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"encoding/xml"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
|
"github.com/versity/versitygw/backend"
|
|
"github.com/versity/versitygw/debuglogger"
|
|
"github.com/versity/versitygw/s3err"
|
|
"github.com/versity/versitygw/s3response"
|
|
)
|
|
|
|
type BucketLockConfig struct {
|
|
Enabled bool
|
|
DefaultRetention *types.DefaultRetention
|
|
CreatedAt *time.Time
|
|
}
|
|
|
|
func ParseBucketLockConfigurationInput(input []byte) ([]byte, error) {
|
|
var lockConfig types.ObjectLockConfiguration
|
|
if err := xml.Unmarshal(input, &lockConfig); err != nil {
|
|
return nil, s3err.GetAPIError(s3err.ErrMalformedXML)
|
|
}
|
|
|
|
if lockConfig.ObjectLockEnabled != types.ObjectLockEnabledEnabled {
|
|
return nil, s3err.GetAPIError(s3err.ErrMalformedXML)
|
|
}
|
|
|
|
config := BucketLockConfig{
|
|
Enabled: lockConfig.ObjectLockEnabled == types.ObjectLockEnabledEnabled,
|
|
}
|
|
|
|
if lockConfig.Rule != nil && lockConfig.Rule.DefaultRetention != nil {
|
|
retention := lockConfig.Rule.DefaultRetention
|
|
|
|
if retention.Mode != types.ObjectLockRetentionModeCompliance && retention.Mode != types.ObjectLockRetentionModeGovernance {
|
|
return nil, s3err.GetAPIError(s3err.ErrMalformedXML)
|
|
}
|
|
if retention.Years != nil && retention.Days != nil {
|
|
return nil, s3err.GetAPIError(s3err.ErrMalformedXML)
|
|
}
|
|
|
|
if retention.Days != nil && *retention.Days <= 0 {
|
|
return nil, s3err.GetInvalidArgumentErr(s3err.InvalidArgObjectLockRetentionDays, fmt.Sprint(*retention.Days))
|
|
}
|
|
if retention.Years != nil && *retention.Years <= 0 {
|
|
return nil, s3err.GetInvalidArgumentErr(s3err.InvalidArgObjectLockRetentionYears, fmt.Sprint(*retention.Years))
|
|
}
|
|
|
|
config.DefaultRetention = retention
|
|
now := time.Now()
|
|
config.CreatedAt = &now
|
|
}
|
|
|
|
return json.Marshal(config)
|
|
}
|
|
|
|
func ParseBucketLockConfigurationOutput(input []byte) (*types.ObjectLockConfiguration, error) {
|
|
var config BucketLockConfig
|
|
if err := json.Unmarshal(input, &config); err != nil {
|
|
return nil, fmt.Errorf("parse object lock config: %w", err)
|
|
}
|
|
|
|
result := &types.ObjectLockConfiguration{
|
|
Rule: &types.ObjectLockRule{
|
|
DefaultRetention: config.DefaultRetention,
|
|
},
|
|
}
|
|
|
|
if config.Enabled {
|
|
result.ObjectLockEnabled = types.ObjectLockEnabledEnabled
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func ParseObjectLockRetentionInput(input []byte) (*s3response.PutObjectRetentionInput, error) {
|
|
var retention s3response.PutObjectRetentionInput
|
|
if err := xml.Unmarshal(input, &retention); err != nil {
|
|
debuglogger.Logf("invalid object lock retention request body: %v", err)
|
|
return nil, s3err.GetAPIError(s3err.ErrMalformedXML)
|
|
}
|
|
|
|
if retention.RetainUntilDate.Before(time.Now()) {
|
|
debuglogger.Logf("object lock retain until date must be in the future")
|
|
return nil, s3err.GetInvalidArgumentErr(s3err.InvalidArgPastObjectLockRetainDate, retention.RetainUntilDate.Format(time.RFC3339))
|
|
}
|
|
switch retention.Mode {
|
|
case types.ObjectLockRetentionModeCompliance:
|
|
case types.ObjectLockRetentionModeGovernance:
|
|
default:
|
|
debuglogger.Logf("invalid object lock retention mode: %s", retention.Mode)
|
|
return nil, s3err.GetAPIError(s3err.ErrMalformedXML)
|
|
}
|
|
|
|
return &retention, nil
|
|
}
|
|
|
|
func ParseObjectLockRetentionInputToJSON(input *s3response.PutObjectRetentionInput) ([]byte, error) {
|
|
data, err := json.Marshal(input)
|
|
if err != nil {
|
|
debuglogger.Logf("parse object lock retention to JSON: %v", err)
|
|
return nil, fmt.Errorf("parse object lock retention: %w", err)
|
|
}
|
|
|
|
return data, nil
|
|
}
|
|
|
|
// IsObjectLockRetentionPutAllowed checks if the object lock retention PUT request
|
|
// is allowed against the current state of the object lock
|
|
func IsObjectLockRetentionPutAllowed(ctx context.Context, be backend.Backend, bucket, object, versionId, userAccess string, input *s3response.PutObjectRetentionInput, bypass bool) error {
|
|
ret, err := be.GetObjectRetention(ctx, bucket, object, versionId)
|
|
if errors.Is(err, s3err.GetAPIError(s3err.ErrNoSuchObjectLockConfiguration)) {
|
|
// if object lock configuration is not set
|
|
// allow the retention modification without any checks
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
debuglogger.Logf("failed to get object retention: %v", err)
|
|
return err
|
|
}
|
|
|
|
retention, err := ParseObjectLockRetentionOutput(ret)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if retention.Mode == input.Mode {
|
|
// if retention mode is the same
|
|
// the operation is allowed
|
|
return nil
|
|
}
|
|
|
|
if retention.Mode == types.ObjectLockRetentionModeCompliance {
|
|
// COMPLIANCE mode is by definition not allowed to modify
|
|
debuglogger.Logf("object lock retention change request from 'COMPLIANCE' to 'GOVERNANCE' is not allowed")
|
|
return s3err.GetAPIError(s3err.ErrObjectLocked)
|
|
}
|
|
|
|
if !bypass {
|
|
// if x-amz-bypass-governance-retention is not provided
|
|
// return error: object is locked
|
|
debuglogger.Logf("object lock retention mode change is not allowed and bypass governence is not forced")
|
|
return s3err.GetAPIError(s3err.ErrObjectLocked)
|
|
}
|
|
|
|
// the last case left, when user tries to chenge
|
|
// from 'GOVERNANCE' to 'COMPLIANCE' with
|
|
// 'x-amz-bypass-governance-retention' header
|
|
// first we need to check if user has 's3:BypassGovernanceRetention'
|
|
policy, err := be.GetBucketPolicy(ctx, bucket)
|
|
if err != nil {
|
|
// if it fails to get the policy, return object is locked
|
|
debuglogger.Logf("failed to get the bucket policy: %v", err)
|
|
return s3err.GetAPIError(s3err.ErrObjectLocked)
|
|
}
|
|
err = VerifyBucketPolicy(policy, userAccess, bucket, object, be.NormalizeObjectKey, BypassGovernanceRetentionAction)
|
|
if err != nil {
|
|
// if user doesn't have "s3:BypassGovernanceRetention" permission
|
|
// return object is locked
|
|
debuglogger.Logf("the user is missing 's3:BypassGovernanceRetention' permission")
|
|
return s3err.GetAPIError(s3err.ErrObjectLocked)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func ParseObjectLockRetentionOutput(input []byte) (*types.ObjectLockRetention, error) {
|
|
var retention types.ObjectLockRetention
|
|
if err := json.Unmarshal(input, &retention); err != nil {
|
|
debuglogger.Logf("parse object lock retention output: %v", err)
|
|
return nil, fmt.Errorf("parse object lock retention: %w", err)
|
|
}
|
|
|
|
return &retention, nil
|
|
}
|
|
|
|
func ParseObjectLegalHoldOutput(status *bool) *s3response.GetObjectLegalHoldResult {
|
|
if status == nil {
|
|
return nil
|
|
}
|
|
|
|
if *status {
|
|
return &s3response.GetObjectLegalHoldResult{
|
|
Status: types.ObjectLockLegalHoldStatusOn,
|
|
}
|
|
}
|
|
|
|
return &s3response.GetObjectLegalHoldResult{
|
|
Status: types.ObjectLockLegalHoldStatusOff,
|
|
}
|
|
}
|
|
|
|
func CheckObjectAccess(ctx context.Context, bucket, userAccess string, objects []types.ObjectIdentifier, bypass, isBucketPublic bool, be backend.Backend, isOverwrite bool) error {
|
|
if isOverwrite {
|
|
// if bucket versioning is enabled, any overwrite request
|
|
// should be enabled, as it leads to a new object version
|
|
// creation
|
|
res, err := be.GetBucketVersioning(ctx, bucket)
|
|
if err == nil && res.Status != nil && *res.Status == types.BucketVersioningStatusEnabled {
|
|
return nil
|
|
}
|
|
}
|
|
data, err := be.GetObjectLockConfiguration(ctx, bucket)
|
|
if err != nil {
|
|
if errors.Is(err, s3err.GetAPIError(s3err.ErrObjectLockConfigurationNotFound)) {
|
|
return nil
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
var bucketLockConfig BucketLockConfig
|
|
if err := json.Unmarshal(data, &bucketLockConfig); err != nil {
|
|
return fmt.Errorf("parse object lock config: %w", err)
|
|
}
|
|
|
|
if !bucketLockConfig.Enabled {
|
|
return nil
|
|
}
|
|
|
|
checkDefaultRetention := false
|
|
|
|
if bucketLockConfig.DefaultRetention != nil && bucketLockConfig.CreatedAt != nil {
|
|
expirationDate := *bucketLockConfig.CreatedAt
|
|
if bucketLockConfig.DefaultRetention.Days != nil {
|
|
expirationDate = expirationDate.AddDate(0, 0, int(*bucketLockConfig.DefaultRetention.Days))
|
|
}
|
|
if bucketLockConfig.DefaultRetention.Years != nil {
|
|
expirationDate = expirationDate.AddDate(int(*bucketLockConfig.DefaultRetention.Years), 0, 0)
|
|
}
|
|
|
|
if expirationDate.After(time.Now()) {
|
|
checkDefaultRetention = true
|
|
}
|
|
}
|
|
|
|
var versioningEnabled bool
|
|
vers, err := be.GetBucketVersioning(ctx, bucket)
|
|
if err == nil && vers.Status != nil {
|
|
versioningEnabled = *vers.Status == types.BucketVersioningStatusEnabled
|
|
}
|
|
|
|
for _, obj := range objects {
|
|
var key, versionId string
|
|
if obj.Key != nil {
|
|
key = *obj.Key
|
|
}
|
|
if obj.VersionId != nil {
|
|
versionId = *obj.VersionId
|
|
}
|
|
// if bucket versioning is enabled and versionId isn't provided
|
|
// no lock check is needed, as it leads to a new delete marker creation
|
|
if versioningEnabled && versionId == "" {
|
|
continue
|
|
}
|
|
checkRetention := true
|
|
retentionData, err := be.GetObjectRetention(ctx, bucket, key, versionId)
|
|
if errors.Is(err, s3err.GetAPIError(s3err.ErrNoSuchKey)) {
|
|
continue
|
|
}
|
|
// the object is a delete marker, if a `MethodNotAllowed` error is returned
|
|
// no object lock check is needed
|
|
if errors.Is(err, s3err.GetAPIError(s3err.ErrMethodNotAllowed)) {
|
|
continue
|
|
}
|
|
if errors.Is(err, s3err.GetAPIError(s3err.ErrNoSuchObjectLockConfiguration)) {
|
|
checkRetention = false
|
|
}
|
|
if err != nil && checkRetention {
|
|
return err
|
|
}
|
|
|
|
if checkRetention {
|
|
retention, err := ParseObjectLockRetentionOutput(retentionData)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if retention.Mode != "" && retention.RetainUntilDate != nil {
|
|
if retention.RetainUntilDate.Before(time.Now()) {
|
|
// if the object retention is expired, the object
|
|
// is allowed for write operations(delete, modify)
|
|
return nil
|
|
}
|
|
|
|
switch retention.Mode {
|
|
case types.ObjectLockRetentionModeGovernance:
|
|
if !bypass {
|
|
return s3err.GetAPIError(s3err.ErrObjectLocked)
|
|
} else {
|
|
policy, err := be.GetBucketPolicy(ctx, bucket)
|
|
if errors.Is(err, s3err.GetAPIError(s3err.ErrNoSuchBucketPolicy)) {
|
|
return s3err.GetAPIError(s3err.ErrObjectLocked)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if isBucketPublic {
|
|
err = VerifyPublicBucketPolicy(policy, bucket, key, be.NormalizeObjectKey, BypassGovernanceRetentionAction)
|
|
} else {
|
|
err = VerifyBucketPolicy(policy, userAccess, bucket, key, be.NormalizeObjectKey, BypassGovernanceRetentionAction)
|
|
}
|
|
if err != nil {
|
|
return s3err.GetAPIError(s3err.ErrObjectLocked)
|
|
}
|
|
}
|
|
case types.ObjectLockRetentionModeCompliance:
|
|
return s3err.GetAPIError(s3err.ErrObjectLocked)
|
|
}
|
|
}
|
|
}
|
|
|
|
checkLegalHold := true
|
|
|
|
status, err := be.GetObjectLegalHold(ctx, bucket, key, versionId)
|
|
if err != nil {
|
|
if errors.Is(err, s3err.GetAPIError(s3err.ErrNoSuchKey)) {
|
|
continue
|
|
}
|
|
if errors.Is(err, s3err.GetAPIError(s3err.ErrNoSuchObjectLockConfiguration)) {
|
|
checkLegalHold = false
|
|
} else {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if checkLegalHold && *status {
|
|
return s3err.GetAPIError(s3err.ErrObjectLocked)
|
|
}
|
|
|
|
if checkDefaultRetention {
|
|
switch bucketLockConfig.DefaultRetention.Mode {
|
|
case types.ObjectLockRetentionModeGovernance:
|
|
if !bypass {
|
|
return s3err.GetAPIError(s3err.ErrObjectLocked)
|
|
} else {
|
|
policy, err := be.GetBucketPolicy(ctx, bucket)
|
|
if errors.Is(err, s3err.GetAPIError(s3err.ErrNoSuchBucketPolicy)) {
|
|
return s3err.GetAPIError(s3err.ErrObjectLocked)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if isBucketPublic {
|
|
err = VerifyPublicBucketPolicy(policy, bucket, key, be.NormalizeObjectKey, BypassGovernanceRetentionAction)
|
|
} else {
|
|
err = VerifyBucketPolicy(policy, userAccess, bucket, key, be.NormalizeObjectKey, BypassGovernanceRetentionAction)
|
|
}
|
|
if err != nil {
|
|
return s3err.GetAPIError(s3err.ErrObjectLocked)
|
|
}
|
|
}
|
|
case types.ObjectLockRetentionModeCompliance:
|
|
return s3err.GetAPIError(s3err.ErrObjectLocked)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|