From: Seena Fallah Date: Thu, 27 Feb 2025 10:53:44 +0000 (+0100) Subject: rgw: take account GetObject(Version)Tagging when replicating X-Git-Url: http://git.apps.os.sepia.ceph.com/?a=commitdiff_plain;h=80959c95475cd8cc871ad74e7f57af742baf042f;p=ceph.git rgw: take account GetObject(Version)Tagging when replicating In case the uid has no permission to read tagging, the tags should not be replicated. Ref. https://docs.aws.amazon.com/AmazonS3/latest/userguide/setting-repl-config-perm-overview.html Signed-off-by: Seena Fallah (cherry picked from commit ae8d7a97714faabe90d1e1660aacabe27e080e42) --- diff --git a/src/rgw/rgw_rest_s3.cc b/src/rgw/rgw_rest_s3.cc index 424dd64e76634..b5516f030d637 100644 --- a/src/rgw/rgw_rest_s3.cc +++ b/src/rgw/rgw_rest_s3.cc @@ -411,6 +411,24 @@ int RGWGetObj_ObjStore_S3::send_response_data(bufferlist& bl, off_t bl_ofs, total_len = 0; } + // check for GetObject(Version)Tagging permission to include tags in response + auto action = s->object->get_instance().empty() ? rgw::IAM::s3GetObjectTagging : rgw::IAM::s3GetObjectVersionTagging; + // since we are already under s->system_request, if the request is not impersonating, + // it can be assumed that it is not a user-mode replication. + bool keep_tags = s->auth.identity->is_admin_of(s->user->get_id()) || verify_object_permission(this, s, action); + + // remove tags from attrs if the user doesn't have permission + bufferlist tags_bl; + if (!keep_tags) { + auto iter = attrs.find(RGW_ATTR_TAGS); + if (iter != attrs.end()) { + ldpp_dout(this, 4) << "removing tags from attrs due to missing permission on " << rgw::IAM::action_bit_string(action) << dendl; + + tags_bl = iter->second; + attrs.erase(iter); + } + } + /* JSON encode object metadata */ JSONFormatter jf; jf.open_object_section("obj_metadata"); @@ -423,6 +441,11 @@ int RGWGetObj_ObjStore_S3::send_response_data(bufferlist& bl, off_t bl_ofs, metadata_bl.append(ss.str()); dump_header(s, "Rgwx-Embedded-Metadata-Len", metadata_bl.length()); total_len += metadata_bl.length(); + + // restore tags + if (tags_bl.length()) { + attrs[RGW_ATTR_TAGS] = std::move(tags_bl); + } } if (s->system_request && !real_clock::is_zero(lastmod)) { diff --git a/src/test/rgw/rgw_multi/tests.py b/src/test/rgw/rgw_multi/tests.py index 88632d13914a0..5bee2b5d1279d 100644 --- a/src/test/rgw/rgw_multi/tests.py +++ b/src/test/rgw/rgw_multi/tests.py @@ -5729,6 +5729,112 @@ def test_bucket_replication_source_forbidden_legalhold(): assert e.response['Error']['Code'] == 'NoSuchKey' # check the source object has replication status set to FAILED - # uncomment me in https://github.com/ceph/ceph/pull/62147 - # res = source.s3_client.head_object(Bucket=source_bucket_name, Key=objname) - # assert_equal(res['ReplicationStatus'], 'FAILED') + +@allow_bucket_replication +def test_bucket_replication_source_forbidden_getobjecttagging(): + zonegroup = realm.master_zonegroup() + zonegroup_conns = ZonegroupConns(zonegroup) + + source = zonegroup_conns.rw_zones[0] + dest = zonegroup_conns.rw_zones[1] + + source_bucket = source.create_bucket(gen_bucket_name()) + dest_bucket = dest.create_bucket(gen_bucket_name()) + zonegroup_meta_checkpoint(zonegroup) + + # create replication configuration + source.s3_client.put_bucket_replication( + Bucket=source_bucket.name, + ReplicationConfiguration={ + 'Role': '', + 'Rules': [{ + 'ID': 'rule1', + 'Status': 'Enabled', + 'Destination': { + 'Bucket': f'arn:aws:s3:::{dest_bucket.name}', + } + }] + } + ) + + # Deny myself from fetching the source object for replication + source.s3_client.put_bucket_policy( + Bucket=source_bucket.name, + Policy=json.dumps({ + 'Version': '2012-10-17', + 'Statement': [{ + 'Effect': 'Deny', + 'Principal': {'AWS': [f"arn:aws:iam:::user/{user.id}"]}, + 'Action': 's3:GetObjectTagging', + 'Resource': f'arn:aws:s3:::{source_bucket.name}/*', + }] + }) + ) + zonegroup_meta_checkpoint(zonegroup) + + # upload an object and wait for sync. + objname = 'dummy' + source.s3_client.put_object(Bucket=source_bucket.name, Key=objname, Body='foo', Tagging='key1=value1') + zone_data_checkpoint(dest.zone, source.zone) + + # check that object exists in destination bucket without tags + res = dest.s3_client.get_object(Bucket=dest_bucket.name, Key=objname) + assert_equal(res['Body'].read().decode('utf-8'), 'foo') + assert 'TagCount' not in res + +@allow_bucket_replication +def test_bucket_replication_source_forbidden_getobjectversiontagging(): + zonegroup = realm.master_zonegroup() + zonegroup_conns = ZonegroupConns(zonegroup) + + source = zonegroup_conns.rw_zones[0] + dest = zonegroup_conns.rw_zones[1] + + source_bucket = source.create_bucket(gen_bucket_name()) + # enable versioning + source.s3_client.put_bucket_versioning( + Bucket=source_bucket.name, + VersioningConfiguration={'Status': 'Enabled'} + ) + dest_bucket = dest.create_bucket(gen_bucket_name()) + zonegroup_meta_checkpoint(zonegroup) + + # create replication configuration + source.s3_client.put_bucket_replication( + Bucket=source_bucket.name, + ReplicationConfiguration={ + 'Role': '', + 'Rules': [{ + 'ID': 'rule1', + 'Status': 'Enabled', + 'Destination': { + 'Bucket': f'arn:aws:s3:::{dest_bucket.name}', + } + }] + } + ) + + # Deny myself from fetching the source object for replication + source.s3_client.put_bucket_policy( + Bucket=source_bucket.name, + Policy=json.dumps({ + 'Version': '2012-10-17', + 'Statement': [{ + 'Effect': 'Deny', + 'Principal': {'AWS': [f"arn:aws:iam:::user/{user.id}"]}, + 'Action': 's3:GetObjectVersionTagging', + 'Resource': f'arn:aws:s3:::{source_bucket.name}/*', + }] + }) + ) + zonegroup_meta_checkpoint(zonegroup) + + # upload an object and wait for sync. + objname = 'dummy' + source.s3_client.put_object(Bucket=source_bucket.name, Key=objname, Body='foo', Tagging='key1=value1') + zone_data_checkpoint(dest.zone, source.zone) + + # check that object exists in destination bucket without tags + res = dest.s3_client.get_object(Bucket=dest_bucket.name, Key=objname) + assert_equal(res['Body'].read().decode('utf-8'), 'foo') + assert 'TagCount' not in res