Skip to content

Commit ac9e76f

Browse files
authored
Merge pull request ceph#55275 from qiuxinyidian/rgw-noti-dev
rgw: add topic owner user check when creating reviewed-by: cbodley, kchheda3
2 parents d21648a + 4581d0d commit ac9e76f

File tree

6 files changed

+156
-30
lines changed

6 files changed

+156
-30
lines changed

PendingReleaseNotes

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,15 @@ CephFS: Disallow delegating preallocated inode ranges to clients. Config
121121
trim the log every second (`mds_log_trim_upkeep_interval` config). Also,
122122
a couple of configs govern how much time the MDS spends in trimming its
123123
logs. These configs are `mds_log_trim_threshold` and `mds_log_trim_decay_rate`.
124+
* RGW: Notification topics are now owned by the user that created them.
125+
By default, only the owner can read/write their topics. Topic policy documents
126+
are now supported to grant these permissions to other users. Preexisting topics
127+
are treated as if they have no owner, and any user can read/write them using the SNS API.
128+
If such a topic is recreated with CreateTopic, the issuing user becomes the new owner.
129+
For backward compatibility, all users still have permission to publish bucket
130+
notifications to topics owned by other users. A new configuration parameter:
131+
``rgw_topic_require_publish_policy`` can be enabled to deny ``sns:Publish``
132+
permissions unless explicitly granted by topic policy.
124133

125134
>=18.0.0
126135

src/common/options/rgw.yaml.in

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3917,15 +3917,15 @@ options:
39173917
services:
39183918
- rgw
39193919
with_legacy: true
3920-
- name: mandatory_topic_permissions
3920+
- name: rgw_topic_require_publish_policy
39213921
type: bool
39223922
level: basic
3923-
desc: Whether to validate user permissions to access notification topics.
3923+
desc: Whether to validate user permissions to publish notifications to topics.
39243924
long_desc: If true, all users (other then the owner of the topic) will need
3925-
to have a policy to access topics.
3925+
to have a policy to publish notifications to topics.
39263926
The topic policy can be set by owner via CreateTopic() or SetTopicAttribute().
39273927
Following permissions can be granted "sns:Publish", "sns:GetTopicAttributes",
3928-
"sns:SetTopicAttributes" and "sns:DeleteTopic" via Policy.
3928+
"sns:SetTopicAttributes", "sns:DeleteTopic" and "sns:CreateTopic" via Policy.
39293929
NOTE that even if set to "false" topics will still follow the policies if set on them.
39303930
default: false
39313931
services:

src/rgw/rgw_iam_policy.cc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ static const actpair actpairs[] =
161161
{ "sns:DeleteTopic", snsDeleteTopic},
162162
{ "sns:Publish", snsPublish},
163163
{ "sns:SetTopicAttributes", snsSetTopicAttributes},
164+
{ "sns:CreateTopic", snsCreateTopic},
164165
};
165166

166167
struct PolicyParser;
@@ -1476,6 +1477,9 @@ const char* action_bit_string(uint64_t action) {
14761477

14771478
case snsPublish:
14781479
return "sns:Publish";
1480+
1481+
case snsCreateTopic:
1482+
return "sns:CreateTopic";
14791483
}
14801484
return "s3Invalid";
14811485
}

src/rgw/rgw_iam_policy.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,8 @@ static constexpr std::uint64_t snsGetTopicAttributes = stsAll + 1;
145145
static constexpr std::uint64_t snsDeleteTopic = stsAll + 2;
146146
static constexpr std::uint64_t snsPublish = stsAll + 3;
147147
static constexpr std::uint64_t snsSetTopicAttributes = stsAll + 4;
148-
static constexpr std::uint64_t snsAll = stsAll + 5;
148+
static constexpr std::uint64_t snsCreateTopic = stsAll + 5;
149+
static constexpr std::uint64_t snsAll = stsAll + 6;
149150

150151
static constexpr std::uint64_t s3Count = s3All;
151152
static constexpr std::uint64_t allCount = snsAll + 1;

src/rgw/rgw_rest_pubsub.cc

Lines changed: 32 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include <algorithm>
55
#include <boost/tokenizer.hpp>
66
#include <optional>
7+
#include "rgw_iam_policy.h"
78
#include "rgw_rest_pubsub.h"
89
#include "rgw_pubsub_push.h"
910
#include "rgw_pubsub.h"
@@ -75,8 +76,8 @@ std::optional<rgw::IAM::Policy> get_policy_from_text(req_state* const s,
7576
s->cct, s->owner.id.tenant, bl,
7677
s->cct->_conf.get_val<bool>("rgw_policy_reject_invalid_principals"));
7778
} catch (rgw::IAM::PolicyParseException& e) {
78-
ldout(s->cct, 1) << "failed to parse policy:' " << policy_text
79-
<< " ' with error: " << e.what() << dendl;
79+
ldout(s->cct, 1) << "failed to parse policy: '" << policy_text
80+
<< "' with error: " << e.what() << dendl;
8081
s->err.message = e.what();
8182
return std::nullopt;
8283
}
@@ -91,13 +92,18 @@ int verify_topic_owner_or_policy(req_state* const s,
9192
}
9293
// no policy set.
9394
if (topic.policy_text.empty()) {
94-
// if mandatory_topic_permissions is true, then validate all users for
95-
// permission.
96-
if (s->cct->_conf->mandatory_topic_permissions) {
97-
return -EACCES;
98-
} else {
95+
// if rgw_topic_require_publish_policy is "false" dont validate "publish" policies
96+
if (op == rgw::IAM::snsPublish && !s->cct->_conf->rgw_topic_require_publish_policy) {
97+
return 0;
98+
}
99+
if (topic.user.empty()) {
100+
// if we don't know the original user and there is no policy
101+
// we will not reject the request.
102+
// this is for compatibility with versions that did not store the user in the topic
99103
return 0;
100104
}
105+
s->err.message = "Topic was created by another user.";
106+
return -EACCES;
101107
}
102108
// bufferlist::static_from_string wants non const string
103109
std::string policy_text(topic.policy_text);
@@ -107,7 +113,7 @@ int verify_topic_owner_or_policy(req_state* const s,
107113
s->user->get_tenant(), topic.name);
108114
if (!p || p->eval(s->env, *s->auth.identity, op, arn, princ_type) !=
109115
rgw::IAM::Effect::Allow) {
110-
ldout(s->cct, 1) << "topic_policy failed validation, topic_policy: " << p
116+
ldout(s->cct, 1) << "topic policy failed validation, topic policy: " << p
111117
<< dendl;
112118
return -EACCES;
113119
}
@@ -195,13 +201,17 @@ class RGWPSCreateTopicOp : public RGWOp {
195201
return 0;
196202
}
197203
if (ret == 0) {
198-
if (result.user == s->owner.id ||
199-
!s->cct->_conf->mandatory_topic_permissions) {
204+
ret = verify_topic_owner_or_policy(
205+
s, result, driver->get_zone()->get_zonegroup().get_name(),
206+
rgw::IAM::snsCreateTopic);
207+
if (ret == 0)
208+
{
200209
return 0;
201210
}
202-
ldpp_dout(this, 1) << "failed to create topic '" << topic_name
211+
212+
ldpp_dout(this, 1) << "no permission to modify topic '" << topic_name
203213
<< "', topic already exist." << dendl;
204-
return -EPERM;
214+
return -EACCES;
205215
}
206216
ldpp_dout(this, 1) << "failed to read topic '" << topic_name
207217
<< "', with error:" << ret << dendl;
@@ -408,8 +418,8 @@ void RGWPSGetTopicOp::execute(optional_yield y) {
408418
s, result, driver->get_zone()->get_zonegroup().get_name(),
409419
rgw::IAM::snsGetTopicAttributes);
410420
if (op_ret != 0) {
411-
ldpp_dout(this, 1) << "failed to get topic '" << topic_name
412-
<< "', topic owned by other user" << dendl;
421+
ldpp_dout(this, 1) << "no permission to get topic '" << topic_name
422+
<< "'" << dendl;
413423
return;
414424
}
415425
ldpp_dout(this, 1) << "successfully got topic '" << topic_name << "'" << dendl;
@@ -492,8 +502,8 @@ void RGWPSGetTopicAttributesOp::execute(optional_yield y) {
492502
s, result, driver->get_zone()->get_zonegroup().get_name(),
493503
rgw::IAM::snsGetTopicAttributes);
494504
if (op_ret != 0) {
495-
ldpp_dout(this, 1) << "failed to get topic '" << topic_name
496-
<< "', topic owned by other user" << dendl;
505+
ldpp_dout(this, 1) << "no permission to get topic '" << topic_name
506+
<< "'" << dendl;
497507
return;
498508
}
499509
ldpp_dout(this, 1) << "successfully got topic '" << topic_name << "'" << dendl;
@@ -617,8 +627,8 @@ class RGWPSSetTopicAttributesOp : public RGWOp {
617627
s, result, driver->get_zone()->get_zonegroup().get_name(),
618628
rgw::IAM::snsSetTopicAttributes);
619629
if (ret != 0) {
620-
ldpp_dout(this, 1) << "failed to set attributes for topic '" << topic_name
621-
<< "', topic owned by other user" << dendl;
630+
ldpp_dout(this, 1) << "no permission to set attributes for topic '" << topic_name
631+
<< "'" << dendl;
622632
return ret;
623633
}
624634

@@ -750,8 +760,8 @@ void RGWPSDeleteTopicOp::execute(optional_yield y) {
750760
s, result, driver->get_zone()->get_zonegroup().get_name(),
751761
rgw::IAM::snsDeleteTopic);
752762
if (op_ret != 0) {
753-
ldpp_dout(this, 1) << "failed to remove topic '" << topic_name
754-
<< "' topic owned by other user" << dendl;
763+
ldpp_dout(this, 1) << "no permission to remove topic '" << topic_name
764+
<< "'" << dendl;
755765
return;
756766
}
757767
} else {
@@ -1025,9 +1035,8 @@ void RGWPSCreateNotifOp::execute(optional_yield y) {
10251035
s, topic_info, driver->get_zone()->get_zonegroup().get_name(),
10261036
rgw::IAM::snsPublish);
10271037
if (op_ret != 0) {
1028-
ldpp_dout(this, 1) << "failed to create notification for topic '"
1029-
<< topic_name << "' topic owned by other user"
1030-
<< dendl;
1038+
ldpp_dout(this, 1) << "no permission to create notification for topic '"
1039+
<< topic_name << "'" << dendl;
10311040
return;
10321041
}
10331042
// make sure that full topic configuration match

src/test/rgw/bucket_notification/test_bn.py

Lines changed: 105 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4395,6 +4395,7 @@ def test_ps_s3_multiple_topics_notification():
43954395
conn.delete_bucket(bucket_name)
43964396
http_server.close()
43974397

4398+
43984399
@attr('basic_test')
43994400
def test_ps_s3_topic_permissions():
44004401
""" test s3 topic set/get/delete permissions """
@@ -4410,7 +4411,7 @@ def test_ps_s3_topic_permissions():
44104411
"Sid": "Statement",
44114412
"Effect": "Deny",
44124413
"Principal": "*",
4413-
"Action": ["sns:Publish", "sns:SetTopicAttributes", "sns:GetTopicAttributes"],
4414+
"Action": ["sns:Publish", "sns:SetTopicAttributes", "sns:GetTopicAttributes", "sns:DeleteTopic", "sns:CreateTopic"],
44144415
"Resource": f"arn:aws:sns:{zonegroup}::{topic_name}"
44154416
}
44164417
]
@@ -4421,10 +4422,23 @@ def test_ps_s3_topic_permissions():
44214422
topic_conf = PSTopicS3(conn1, topic_name, zonegroup, endpoint_args=endpoint_args, policy_text=topic_policy)
44224423
topic_arn = topic_conf.set_config()
44234424

4424-
# 2nd user tries to fetch the topic
44254425
topic_conf2 = PSTopicS3(conn2, topic_name, zonegroup, endpoint_args=endpoint_args)
4426+
try:
4427+
# 2nd user tries to override the topic
4428+
topic_arn = topic_conf2.set_config()
4429+
assert False, "'AccessDenied' error is expected"
4430+
except ClientError as err:
4431+
if 'Error' in err.response:
4432+
assert_equal(err.response['Error']['Code'], 'AccessDenied')
4433+
else:
4434+
assert_equal(err.response['Code'], 'AccessDenied')
4435+
except Exception as err:
4436+
print('unexpected error type: '+type(err).__name__)
4437+
4438+
# 2nd user tries to fetch the topic
44264439
_, status = topic_conf2.get_config(topic_arn=topic_arn)
44274440
assert_equal(status, 403)
4441+
44284442
try:
44294443
# 2nd user tries to set the attribute
44304444
status = topic_conf2.set_attributes(attribute_name="persistent", attribute_val="false", topic_arn=topic_arn)
@@ -4455,6 +4469,18 @@ def test_ps_s3_topic_permissions():
44554469
except Exception as err:
44564470
print('unexpected error type: '+type(err).__name__)
44574471

4472+
try:
4473+
# 2nd user tries to delete the topic
4474+
status = topic_conf2.del_config(topic_arn=topic_arn)
4475+
assert False, "'AccessDenied' error is expected"
4476+
except ClientError as err:
4477+
if 'Error' in err.response:
4478+
assert_equal(err.response['Error']['Code'], 'AccessDenied')
4479+
else:
4480+
assert_equal(err.response['Code'], 'AccessDenied')
4481+
except Exception as err:
4482+
print('unexpected error type: '+type(err).__name__)
4483+
44584484
# Topic policy is now added by the 1st user to allow 2nd user.
44594485
topic_policy = topic_policy.replace("Deny", "Allow")
44604486
topic_conf = PSTopicS3(conn1, topic_name, zonegroup, endpoint_args=endpoint_args, policy_text=topic_policy)
@@ -4469,13 +4495,90 @@ def test_ps_s3_topic_permissions():
44694495
s3_notification_conf2 = PSNotificationS3(conn2, bucket_name, topic_conf_list)
44704496
_, status = s3_notification_conf2.set_config()
44714497
assert_equal(status, 200)
4498+
# 2nd user tries to delete the topic again
4499+
status = topic_conf2.del_config(topic_arn=topic_arn)
4500+
assert_equal(status, 200)
4501+
4502+
# cleanup
4503+
s3_notification_conf2.del_config()
4504+
# delete the bucket
4505+
conn2.delete_bucket(bucket_name)
4506+
4507+
4508+
@attr('basic_test')
4509+
def test_ps_s3_topic_no_permissions():
4510+
""" test s3 topic set/get/delete permissions """
4511+
conn1 = connection()
4512+
conn2 = another_user()
4513+
zonegroup = 'default'
4514+
bucket_name = gen_bucket_name()
4515+
topic_name = bucket_name + TOPIC_SUFFIX
4516+
4517+
# create s3 topic without policy
4518+
endpoint_address = 'amqp://127.0.0.1:7001'
4519+
endpoint_args = 'push-endpoint='+endpoint_address+'&amqp-exchange=amqp.direct&amqp-ack-level=none'
4520+
topic_conf = PSTopicS3(conn1, topic_name, zonegroup, endpoint_args=endpoint_args)
4521+
topic_arn = topic_conf.set_config()
4522+
4523+
topic_conf2 = PSTopicS3(conn2, topic_name, zonegroup, endpoint_args=endpoint_args)
4524+
try:
4525+
# 2nd user tries to override the topic
4526+
topic_arn = topic_conf2.set_config()
4527+
assert False, "'AccessDenied' error is expected"
4528+
except ClientError as err:
4529+
if 'Error' in err.response:
4530+
assert_equal(err.response['Error']['Code'], 'AccessDenied')
4531+
else:
4532+
assert_equal(err.response['Code'], 'AccessDenied')
4533+
except Exception as err:
4534+
print('unexpected error type: '+type(err).__name__)
4535+
4536+
# 2nd user tries to fetch the topic
4537+
_, status = topic_conf2.get_config(topic_arn=topic_arn)
4538+
assert_equal(status, 403)
4539+
4540+
try:
4541+
# 2nd user tries to set the attribute
4542+
status = topic_conf2.set_attributes(attribute_name="persistent", attribute_val="false", topic_arn=topic_arn)
4543+
assert False, "'AccessDenied' error is expected"
4544+
except ClientError as err:
4545+
if 'Error' in err.response:
4546+
assert_equal(err.response['Error']['Code'], 'AccessDenied')
4547+
else:
4548+
assert_equal(err.response['Code'], 'AccessDenied')
4549+
except Exception as err:
4550+
print('unexpected error type: '+type(err).__name__)
4551+
4552+
# create bucket for conn2 publish notification to topic
4553+
# should be allowed based on the default value of rgw_topic_require_publish_policy=false
4554+
_ = conn2.create_bucket(bucket_name)
4555+
notification_name = bucket_name + NOTIFICATION_SUFFIX
4556+
topic_conf_list = [{'Id': notification_name, 'TopicArn': topic_arn,
4557+
'Events': []
4558+
}]
4559+
s3_notification_conf2 = PSNotificationS3(conn2, bucket_name, topic_conf_list)
4560+
_, status = s3_notification_conf2.set_config()
4561+
assert_equal(status, 200)
4562+
4563+
try:
4564+
# 2nd user tries to delete the topic
4565+
status = topic_conf2.del_config(topic_arn=topic_arn)
4566+
assert False, "'AccessDenied' error is expected"
4567+
except ClientError as err:
4568+
if 'Error' in err.response:
4569+
assert_equal(err.response['Error']['Code'], 'AccessDenied')
4570+
else:
4571+
assert_equal(err.response['Code'], 'AccessDenied')
4572+
except Exception as err:
4573+
print('unexpected error type: '+type(err).__name__)
44724574

44734575
# cleanup
44744576
s3_notification_conf2.del_config()
44754577
topic_conf.del_config()
44764578
# delete the bucket
44774579
conn2.delete_bucket(bucket_name)
44784580

4581+
44794582
def kafka_security(security_type, mechanism='PLAIN'):
44804583
""" test pushing kafka s3 notification securly to master """
44814584
conn = connection()

0 commit comments

Comments
 (0)