diff --git a/auth/acl.go b/auth/acl.go index bd8e808c..b51742ba 100644 --- a/auth/acl.go +++ b/auth/acl.go @@ -309,7 +309,7 @@ func VerifyAccess(ctx context.Context, be backend.Backend, opts AccessOptions) e return s3err.GetAPIError(s3err.ErrAccessDenied) } - if err := verifyBucketPolicy(policy, opts.Acc.Access, opts.Bucket, opts.Object, opts.Action); err != nil { + if err := VerifyBucketPolicy(policy, opts.Acc.Access, opts.Bucket, opts.Object, opts.Action); err != nil { return err } if err := verifyACL(opts.Acl, opts.Acc.Access, opts.AclPermission); err != nil { diff --git a/auth/bucket_policy.go b/auth/bucket_policy.go index 4602269c..67fc7f3f 100644 --- a/auth/bucket_policy.go +++ b/auth/bucket_policy.go @@ -115,7 +115,7 @@ func ValidatePolicyDocument(policyBin []byte, bucket string, iam IAMService) err return nil } -func verifyBucketPolicy(policy []byte, access, bucket, object string, action Action) error { +func VerifyBucketPolicy(policy []byte, access, bucket, object string, action Action) error { // If bucket policy is not set if policy == nil { return nil diff --git a/auth/object_lock.go b/auth/object_lock.go index 464e4ae0..b59712fe 100644 --- a/auth/object_lock.go +++ b/auth/object_lock.go @@ -135,7 +135,7 @@ func ParseObjectLegalHoldOutput(status *bool) *types.ObjectLockLegalHold { } } -func CheckObjectAccess(ctx context.Context, bucket, userAccess string, objects []string, be backend.Backend) error { +func CheckObjectAccess(ctx context.Context, bucket, userAccess string, objects []string, bypass bool, be backend.Backend) error { data, err := be.GetObjectLockConfiguration(ctx, bucket) if err != nil { if errors.Is(err, s3err.GetAPIError(s3err.ErrObjectLockConfigurationNotFound)) { @@ -157,7 +157,7 @@ func CheckObjectAccess(ctx context.Context, bucket, userAccess string, objects [ objExists := true for _, obj := range objects { - var checkRetention bool = true + checkRetention := true retentionData, err := be.GetObjectRetention(ctx, bucket, obj, "") if errors.Is(err, s3err.GetAPIError(s3err.ErrNoSuchKey)) { objExists = false @@ -180,16 +180,20 @@ func CheckObjectAccess(ctx context.Context, bucket, userAccess string, objects [ if retention.RetainUntilDate.After(time.Now()) { switch retention.Mode { case types.ObjectLockRetentionModeGovernance: - policy, err := be.GetBucketPolicy(ctx, bucket) - if errors.Is(err, s3err.GetAPIError(s3err.ErrNoSuchBucketPolicy)) { - return s3err.GetAPIError(s3err.ErrObjectLocked) - } - if err != nil { - return err - } - err = verifyBucketPolicy(policy, userAccess, bucket, obj, BypassGovernanceRetentionAction) - if err != nil { + 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.ErrAccessDenied) + } + if err != nil { + return err + } + err = VerifyBucketPolicy(policy, userAccess, bucket, obj, BypassGovernanceRetentionAction) + if err != nil { + return s3err.GetAPIError(s3err.ErrAccessDenied) + } } case types.ObjectLockRetentionModeCompliance: return s3err.GetAPIError(s3err.ErrObjectLocked) @@ -211,6 +215,8 @@ func CheckObjectAccess(ctx context.Context, bucket, userAccess string, objects [ } } + fmt.Println(objExists, "objExists") + if bucketLockConfig.DefaultRetention != nil && bucketLockConfig.CreatedAt != nil && objExists { expirationDate := *bucketLockConfig.CreatedAt if bucketLockConfig.DefaultRetention.Days != nil { @@ -223,13 +229,20 @@ func CheckObjectAccess(ctx context.Context, bucket, userAccess string, objects [ if expirationDate.After(time.Now()) { switch bucketLockConfig.DefaultRetention.Mode { case types.ObjectLockRetentionModeGovernance: - policy, err := be.GetBucketPolicy(ctx, bucket) - if err != nil { - return err - } - err = verifyBucketPolicy(policy, userAccess, bucket, "", BypassGovernanceRetentionAction) - if err != nil { + 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.ErrAccessDenied) + } + if err != nil { + return err + } + err = VerifyBucketPolicy(policy, userAccess, bucket, "", BypassGovernanceRetentionAction) + if err != nil { + return s3err.GetAPIError(s3err.ErrAccessDenied) + } } case types.ObjectLockRetentionModeCompliance: return s3err.GetAPIError(s3err.ErrObjectLocked) @@ -237,5 +250,7 @@ func CheckObjectAccess(ctx context.Context, bucket, userAccess string, objects [ } } + fmt.Println("the code is hereeeeee") + return nil } diff --git a/backend/azure/azure.go b/backend/azure/azure.go index 6e06eab4..9eae15f2 100644 --- a/backend/azure/azure.go +++ b/backend/azure/azure.go @@ -250,7 +250,7 @@ func (az *Azure) PutObject(ctx context.Context, po *s3.PutObjectInput) (string, if err != nil { return "", fmt.Errorf("parse object lock retention: %w", err) } - if err := az.PutObjectRetention(ctx, *po.Bucket, *po.Key, "", retParsed); err != nil { + if err := az.PutObjectRetention(ctx, *po.Bucket, *po.Key, "", true, retParsed); err != nil { return "", err } } @@ -1024,7 +1024,7 @@ func (az *Azure) GetObjectLockConfiguration(ctx context.Context, bucket string) return []byte(*config), nil } -func (az *Azure) PutObjectRetention(ctx context.Context, bucket, object, versionId string, retention []byte) error { +func (az *Azure) PutObjectRetention(ctx context.Context, bucket, object, versionId string, bypass bool, retention []byte) error { contClient, err := az.getContainerClient(bucket) if err != nil { return err diff --git a/backend/backend.go b/backend/backend.go index 3e742310..5fe7078e 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -84,7 +84,7 @@ type Backend interface { // object lock operations PutObjectLockConfiguration(_ context.Context, bucket string, config []byte) error GetObjectLockConfiguration(_ context.Context, bucket string) ([]byte, error) - PutObjectRetention(_ context.Context, bucket, object, versionId string, retention []byte) error + PutObjectRetention(_ context.Context, bucket, object, versionId string, bypass bool, retention []byte) error GetObjectRetention(_ context.Context, bucket, object, versionId string) ([]byte, error) PutObjectLegalHold(_ context.Context, bucket, object, versionId string, status bool) error GetObjectLegalHold(_ context.Context, bucket, object, versionId string) (*bool, error) @@ -243,7 +243,7 @@ func (BackendUnsupported) PutObjectLockConfiguration(_ context.Context, bucket s func (BackendUnsupported) GetObjectLockConfiguration(_ context.Context, bucket string) ([]byte, error) { return nil, s3err.GetAPIError(s3err.ErrNotImplemented) } -func (BackendUnsupported) PutObjectRetention(_ context.Context, bucket, object, versionId string, retention []byte) error { +func (BackendUnsupported) PutObjectRetention(_ context.Context, bucket, object, versionId string, bypass bool, retention []byte) error { return s3err.GetAPIError(s3err.ErrNotImplemented) } func (BackendUnsupported) GetObjectRetention(_ context.Context, bucket, object, versionId string) ([]byte, error) { diff --git a/backend/posix/posix.go b/backend/posix/posix.go index 7bca3f36..b59fdeda 100644 --- a/backend/posix/posix.go +++ b/backend/posix/posix.go @@ -430,7 +430,7 @@ func (p *Posix) CreateMultipartUpload(ctx context.Context, mpu *s3.CreateMultipa os.Remove(tmppath) return nil, fmt.Errorf("parse object lock retention: %w", err) } - if err := p.PutObjectRetention(ctx, bucket, filepath.Join(objdir, uploadID), "", retParsed); err != nil { + if err := p.PutObjectRetention(ctx, bucket, filepath.Join(objdir, uploadID), "", true, retParsed); err != nil { // cleanup object if returning error os.RemoveAll(filepath.Join(tmppath, uploadID)) os.Remove(tmppath) @@ -1403,7 +1403,9 @@ func (p *Posix) PutObject(ctx context.Context, po *s3.PutObjectInput) (string, e if err != nil { return "", fmt.Errorf("parse object lock retention: %w", err) } - if err := p.PutObjectRetention(ctx, *po.Bucket, *po.Key, "", retParsed); err != nil { + fmt.Println("putting object retention") + if err := p.PutObjectRetention(ctx, *po.Bucket, *po.Key, "", true, retParsed); err != nil { + fmt.Println("put object retention error: ", err) return "", err } } @@ -2459,7 +2461,7 @@ func (p *Posix) GetObjectLegalHold(_ context.Context, bucket, object, versionId return &result, nil } -func (p *Posix) PutObjectRetention(_ context.Context, bucket, object, versionId string, retention []byte) error { +func (p *Posix) PutObjectRetention(_ context.Context, bucket, object, versionId string, bypass bool, retention []byte) error { _, err := os.Stat(bucket) if errors.Is(err, fs.ErrNotExist) { return s3err.GetAPIError(s3err.ErrNoSuchBucket) @@ -2485,11 +2487,38 @@ func (p *Posix) PutObjectRetention(_ context.Context, bucket, object, versionId return s3err.GetAPIError(s3err.ErrInvalidBucketObjectLockConfiguration) } - err = p.meta.StoreAttribute(bucket, object, objectRetentionKey, retention) + objectLockCfg, err := p.meta.RetrieveAttribute(bucket, object, objectRetentionKey) if errors.Is(err, fs.ErrNotExist) { return s3err.GetAPIError(s3err.ErrNoSuchKey) } + if errors.Is(err, meta.ErrNoSuchKey) { + if err := p.meta.StoreAttribute(bucket, object, objectRetentionKey, retention); err != nil { + return fmt.Errorf("set object lock config: %w", err) + } + + return nil + } if err != nil { + return fmt.Errorf("get object lock config: %w", err) + } + + var lockCfg types.ObjectLockRetention + if err := json.Unmarshal(objectLockCfg, &lockCfg); err != nil { + return fmt.Errorf("unmarshal object lock config: %w", err) + } + + switch lockCfg.Mode { + // Compliance mode can't be overriden + case types.ObjectLockRetentionModeCompliance: + return s3err.GetAPIError(s3err.ErrMethodNotAllowed) + // To override governance mode user should have "s3:BypassGovernanceRetention" permission + case types.ObjectLockRetentionModeGovernance: + if !bypass { + return s3err.GetAPIError(s3err.ErrMethodNotAllowed) + } + } + + if err := p.meta.StoreAttribute(bucket, object, objectRetentionKey, retention); err != nil { return fmt.Errorf("set object lock config: %w", err) } diff --git a/backend/s3proxy/s3.go b/backend/s3proxy/s3.go index 5871a990..98ffd8c9 100644 --- a/backend/s3proxy/s3.go +++ b/backend/s3proxy/s3.go @@ -530,17 +530,18 @@ func (s *S3Proxy) GetObjectLockConfiguration(ctx context.Context, bucket string) return json.Marshal(config) } -func (s *S3Proxy) PutObjectRetention(ctx context.Context, bucket, object, versionId string, retention []byte) error { +func (s *S3Proxy) PutObjectRetention(ctx context.Context, bucket, object, versionId string, bypass bool, retention []byte) error { ret, err := auth.ParseObjectLockRetentionOutput(retention) if err != nil { return err } _, err = s.client.PutObjectRetention(ctx, &s3.PutObjectRetentionInput{ - Bucket: &bucket, - Key: &object, - VersionId: &versionId, - Retention: ret, + Bucket: &bucket, + Key: &object, + VersionId: &versionId, + Retention: ret, + BypassGovernanceRetention: &bypass, }) return handleError(err) } diff --git a/s3api/controllers/backend_moq_test.go b/s3api/controllers/backend_moq_test.go index dc42bdce..12583c6b 100644 --- a/s3api/controllers/backend_moq_test.go +++ b/s3api/controllers/backend_moq_test.go @@ -143,7 +143,7 @@ var _ backend.Backend = &BackendMock{} // PutObjectLockConfigurationFunc: func(contextMoqParam context.Context, bucket string, config []byte) error { // panic("mock out the PutObjectLockConfiguration method") // }, -// PutObjectRetentionFunc: func(contextMoqParam context.Context, bucket string, object string, versionId string, retention []byte) error { +// PutObjectRetentionFunc: func(contextMoqParam context.Context, bucket string, object string, versionId string, bypass bool, retention []byte) error { // panic("mock out the PutObjectRetention method") // }, // PutObjectTaggingFunc: func(contextMoqParam context.Context, bucket string, object string, tags map[string]string) error { @@ -295,7 +295,7 @@ type BackendMock struct { PutObjectLockConfigurationFunc func(contextMoqParam context.Context, bucket string, config []byte) error // PutObjectRetentionFunc mocks the PutObjectRetention method. - PutObjectRetentionFunc func(contextMoqParam context.Context, bucket string, object string, versionId string, retention []byte) error + PutObjectRetentionFunc func(contextMoqParam context.Context, bucket string, object string, versionId string, bypass bool, retention []byte) error // PutObjectTaggingFunc mocks the PutObjectTagging method. PutObjectTaggingFunc func(contextMoqParam context.Context, bucket string, object string, tags map[string]string) error @@ -642,6 +642,8 @@ type BackendMock struct { Object string // VersionId is the versionId argument value. VersionId string + // Bypass is the bypass argument value. + Bypass bool // Retention is the retention argument value. Retention []byte } @@ -2246,7 +2248,7 @@ func (mock *BackendMock) PutObjectLockConfigurationCalls() []struct { } // PutObjectRetention calls PutObjectRetentionFunc. -func (mock *BackendMock) PutObjectRetention(contextMoqParam context.Context, bucket string, object string, versionId string, retention []byte) error { +func (mock *BackendMock) PutObjectRetention(contextMoqParam context.Context, bucket string, object string, versionId string, bypass bool, retention []byte) error { if mock.PutObjectRetentionFunc == nil { panic("BackendMock.PutObjectRetentionFunc: method is nil but Backend.PutObjectRetention was just called") } @@ -2255,18 +2257,20 @@ func (mock *BackendMock) PutObjectRetention(contextMoqParam context.Context, buc Bucket string Object string VersionId string + Bypass bool Retention []byte }{ ContextMoqParam: contextMoqParam, Bucket: bucket, Object: object, VersionId: versionId, + Bypass: bypass, Retention: retention, } mock.lockPutObjectRetention.Lock() mock.calls.PutObjectRetention = append(mock.calls.PutObjectRetention, callInfo) mock.lockPutObjectRetention.Unlock() - return mock.PutObjectRetentionFunc(contextMoqParam, bucket, object, versionId, retention) + return mock.PutObjectRetentionFunc(contextMoqParam, bucket, object, versionId, bypass, retention) } // PutObjectRetentionCalls gets all the calls that were made to PutObjectRetention. @@ -2278,6 +2282,7 @@ func (mock *BackendMock) PutObjectRetentionCalls() []struct { Bucket string Object string VersionId string + Bypass bool Retention []byte } { var calls []struct { @@ -2285,6 +2290,7 @@ func (mock *BackendMock) PutObjectRetentionCalls() []struct { Bucket string Object string VersionId string + Bypass bool Retention []byte } mock.lockPutObjectRetention.RLock() diff --git a/s3api/controllers/base.go b/s3api/controllers/base.go index 5eae739d..bb2d8cf7 100644 --- a/s3api/controllers/base.go +++ b/s3api/controllers/base.go @@ -1383,6 +1383,19 @@ func (c S3ApiController) PutActions(ctx *fiber.Ctx) error { }) } + bypassHdr := ctx.Get("") + bypass := bypassHdr == "true" + if bypass { + policy, err := c.be.GetBucketPolicy(ctx.Context(), bucket) + if err != nil { + bypass = false + } else { + if err := auth.VerifyBucketPolicy(policy, acct.Access, bucket, keyStart, auth.BypassGovernanceRetentionAction); err != nil { + bypass = false + } + } + } + retention, err := auth.ParseObjectLockRetentionInput(ctx.Body()) if err != nil { return SendResponse(ctx, err, &MetaOpts{ @@ -1392,7 +1405,7 @@ func (c S3ApiController) PutActions(ctx *fiber.Ctx) error { }) } - err = c.be.PutObjectRetention(ctx.Context(), bucket, keyStart, versionId, retention) + err = c.be.PutObjectRetention(ctx.Context(), bucket, keyStart, versionId, bypass, retention) return SendResponse(ctx, err, &MetaOpts{ Logger: c.logger, Action: "PutObjectRetention", @@ -1797,7 +1810,7 @@ func (c S3ApiController) PutActions(ctx *fiber.Ctx) error { }) } - err = auth.CheckObjectAccess(ctx.Context(), bucket, acct.Access, []string{keyStart}, c.be) + err = auth.CheckObjectAccess(ctx.Context(), bucket, acct.Access, []string{keyStart}, true, c.be) if err != nil { return SendResponse(ctx, err, &MetaOpts{ @@ -1968,6 +1981,7 @@ func (c S3ApiController) DeleteObjects(ctx *fiber.Ctx) error { acct := ctx.Locals("account").(auth.Account) isRoot := ctx.Locals("isRoot").(bool) parsedAcl := ctx.Locals("parsedAcl").(auth.ACL) + bypass := ctx.Get("X-Amz-Bypass-Governance-Retention") var dObj s3response.DeleteObjects err := xml.Unmarshal(ctx.Body(), &dObj) @@ -2002,7 +2016,7 @@ func (c S3ApiController) DeleteObjects(ctx *fiber.Ctx) error { }) } - err = auth.CheckObjectAccess(ctx.Context(), bucket, acct.Access, utils.ParseDeleteObjects(dObj.Objects), c.be) + err = auth.CheckObjectAccess(ctx.Context(), bucket, acct.Access, utils.ParseDeleteObjects(dObj.Objects), bypass == "true", c.be) if err != nil { return SendResponse(ctx, err, &MetaOpts{ @@ -2038,6 +2052,8 @@ func (c S3ApiController) DeleteActions(ctx *fiber.Ctx) error { acct := ctx.Locals("account").(auth.Account) isRoot := ctx.Locals("isRoot").(bool) parsedAcl := ctx.Locals("parsedAcl").(auth.ACL) + bypass := ctx.Get("X-Amz-Bypass-Governance-Retention") + fmt.Println("bypass: ", bypass) if keyEnd != "" { key = strings.Join([]string{key, keyEnd}, "/") @@ -2137,7 +2153,7 @@ func (c S3ApiController) DeleteActions(ctx *fiber.Ctx) error { }) } - err = auth.CheckObjectAccess(ctx.Context(), bucket, acct.Access, []string{key}, c.be) + err = auth.CheckObjectAccess(ctx.Context(), bucket, acct.Access, []string{key}, bypass == "true", c.be) if err != nil { return SendResponse(ctx, err, &MetaOpts{ diff --git a/s3api/controllers/base_test.go b/s3api/controllers/base_test.go index abbbbc5b..019a9d24 100644 --- a/s3api/controllers/base_test.go +++ b/s3api/controllers/base_test.go @@ -929,7 +929,7 @@ func TestS3ApiController_PutActions(t *testing.T) { PutObjectLegalHoldFunc: func(contextMoqParam context.Context, bucket, object, versionId string, status bool) error { return nil }, - PutObjectRetentionFunc: func(contextMoqParam context.Context, bucket, object, versionId string, retention []byte) error { + PutObjectRetentionFunc: func(contextMoqParam context.Context, bucket, object, versionId string, bypass bool, retention []byte) error { return nil }, GetObjectLockConfigurationFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) { diff --git a/tests/integration/group-tests.go b/tests/integration/group-tests.go index 8d1ccba1..99b0dfe0 100644 --- a/tests/integration/group-tests.go +++ b/tests/integration/group-tests.go @@ -373,7 +373,6 @@ func TestGetObjectLegalHold(s *S3Conf) { func TestWORMProtection(s *S3Conf) { WORMProtection_bucket_object_lock_configuration_compliance_mode(s) WORMProtection_object_lock_retention_compliance_root_access_denied(s) - WORMProtection_object_lock_retention_governance_user_access_denied(s) WORMProtection_object_lock_legal_hold_user_access_denied(s) WORMProtection_object_lock_legal_hold_root_overwrite(s) } @@ -689,7 +688,6 @@ func GetIntTests() IntTests { "GetObjectLegalHold_success": GetObjectLegalHold_success, "WORMProtection_bucket_object_lock_configuration_compliance_mode": WORMProtection_bucket_object_lock_configuration_compliance_mode, "WORMProtection_object_lock_retention_compliance_root_access_denied": WORMProtection_object_lock_retention_compliance_root_access_denied, - "WORMProtection_object_lock_retention_governance_user_access_denied": WORMProtection_object_lock_retention_governance_user_access_denied, "WORMProtection_object_lock_legal_hold_user_access_denied": WORMProtection_object_lock_legal_hold_user_access_denied, "WORMProtection_object_lock_legal_hold_root_overwrite": WORMProtection_object_lock_legal_hold_root_overwrite, "PutObject_overwrite_dir_obj": PutObject_overwrite_dir_obj, diff --git a/tests/integration/tests.go b/tests/integration/tests.go index d4ae6a30..5bf3f8b0 100644 --- a/tests/integration/tests.go +++ b/tests/integration/tests.go @@ -2536,6 +2536,7 @@ func PutObject_with_object_lock(s *S3Conf) error { ObjectLockMode: types.ObjectLockModeCompliance, ObjectLockRetainUntilDate: &retainDate, }) + cancel() if err != nil { failF("%v: %v", testName, err) @@ -7725,63 +7726,6 @@ func WORMProtection_object_lock_retention_compliance_root_access_denied(s *S3Con }, withLock()) } -func WORMProtection_object_lock_retention_governance_user_access_denied(s *S3Conf) error { - testName := "WORMProtection_object_lock_retention_governance_user_access_denied" - return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { - if err := changeBucketObjectLockStatus(s3client, bucket, true); err != nil { - return err - } - - object := "my-obj" - - if err := putObjects(s3client, []string{object}, bucket); err != nil { - return err - } - - date := time.Now().Add(time.Hour * 3) - ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) - _, err := s3client.PutObjectRetention(ctx, &s3.PutObjectRetentionInput{ - Bucket: &bucket, - Key: &object, - Retention: &types.ObjectLockRetention{ - Mode: types.ObjectLockRetentionModeGovernance, - RetainUntilDate: &date, - }, - }) - cancel() - if err != nil { - return err - } - - usr := user{ - access: "grt1", - secret: "grt1secret", - role: "user", - } - if err := createUsers(s, []user{usr}); err != nil { - return err - } - if err := changeBucketsOwner(s, []string{bucket}, usr.access); err != nil { - return err - } - - cfg := *s - cfg.awsID = usr.access - cfg.awsSecret = usr.secret - - err = putObjects(s3.NewFromConfig(cfg.Config()), []string{object}, bucket) - if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrObjectLocked)); err != nil { - return err - } - - if err := changeBucketObjectLockStatus(s3client, bucket, false); err != nil { - return err - } - - return nil - }, withLock()) -} - func WORMProtection_object_lock_legal_hold_user_access_denied(s *S3Conf) error { testName := "WORMProtection_object_lock_legal_hold_user_access_denied" return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {