Skip to content

Commit d4c951f

Browse files
committed
MB-28825: Let command timings be protected by SimpleStats privilege
Users with SimpleStats (assigned by DataMonitoring role) may query the bucket for command timings for all of the buckets they have SimpleStats privilege. This means that if user foo with access to bucket1 and bucket2, but not bucket3 tries to run: mctimings -v GET or mctimings -v -b /all/ GET The aggregated stats of bucket1 and bucket2 is returned Change-Id: Ia69fc1582cd7add4d972bb9bf99a84181f7330c5 Reviewed-on: http://review.couchbase.org/91494 Well-Formed: Build Bot <[email protected]> Reviewed-by: Dave Rigby <[email protected]> Tested-by: Build Bot <[email protected]>
1 parent 3c8b5eb commit d4c951f

File tree

8 files changed

+340
-35
lines changed

8 files changed

+340
-35
lines changed

daemon/mcbp_privileges.cc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ McbpPrivilegeChains::McbpPrivilegeChains() {
283283
setup(PROTOCOL_BINARY_CMD_SSL_CERTS_REFRESH,
284284
require<Privilege::SecurityManagement>);
285285
/* Internal timer ioctl */
286-
setup(PROTOCOL_BINARY_CMD_GET_CMD_TIMER, require<Privilege::NodeManagement>);
286+
setup(PROTOCOL_BINARY_CMD_GET_CMD_TIMER, empty);
287287
/* ns_server - memcached session validation */
288288
setup(PROTOCOL_BINARY_CMD_SET_CTRL_TOKEN, require<Privilege::SessionManagement>);
289289
setup(PROTOCOL_BINARY_CMD_GET_CTRL_TOKEN, require<Privilege::SessionManagement>);

daemon/protocol/mcbp/get_cmd_timer_executor.cc

Lines changed: 123 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,105 @@
2121
#include <daemon/mcbp.h>
2222
#include <daemon/buckets.h>
2323

24+
/**
25+
* Get the timing histogram for the specified bucket if we've got access
26+
* to the bucket.
27+
*
28+
* @param connection The connection executing the command
29+
* @param bucket The bucket to get the timing data from
30+
* @param opcode The opcode to get the timing histogram for
31+
* @return A std::pair with the first being the error code for the operation
32+
* and the second being the histogram (only valid if the first
33+
* parameter is ENGINE_SUCCESS)
34+
*/
35+
static std::pair<ENGINE_ERROR_CODE, TimingHistogram> get_timings(
36+
McbpConnection& connection, const Bucket& bucket, uint8_t opcode) {
37+
// Don't creata a new privilege context if the one we've got is for the
38+
// connected bucket:
39+
if (bucket.name == connection.getBucket().name) {
40+
auto ret = mcbp::checkPrivilege(connection,
41+
cb::rbac::Privilege::SimpleStats);
42+
if (ret != ENGINE_SUCCESS) {
43+
return std::make_pair(ENGINE_EACCESS, TimingHistogram{});
44+
}
45+
} else {
46+
// Check to see if we've got access to the bucket
47+
bool access = false;
48+
try {
49+
auto context = cb::rbac::createContext(connection.getUsername(),
50+
bucket.name);
51+
const auto check = context.check(cb::rbac::Privilege::SimpleStats);
52+
if (check == cb::rbac::PrivilegeAccess::Ok) {
53+
access = true;
54+
}
55+
} catch (const cb::rbac::Exception& e) {
56+
// We don't have access to that bucket
57+
}
58+
59+
if (!access) {
60+
return std::make_pair(ENGINE_EACCESS, TimingHistogram{});
61+
}
62+
}
63+
64+
return std::make_pair(ENGINE_SUCCESS,
65+
bucket.timings.get_timing_histogram(opcode));
66+
}
67+
68+
/**
69+
* Get the command timings for the provided bucket if:
70+
* it's not NoBucket
71+
* it is running
72+
* it has the given name
73+
*
74+
* @param connection The connection requesting access
75+
* @param bucket The bucket to look at
76+
* @param opcode The opcode we're interested in
77+
* @param bucketname The name of the bucket we want
78+
*/
79+
static std::pair<ENGINE_ERROR_CODE, TimingHistogram> maybe_get_timings(
80+
McbpConnection& connection, const Bucket& bucket, uint8_t opcode, const std::string& bucketname) {
81+
82+
std::pair<ENGINE_ERROR_CODE, TimingHistogram> ret = std::make_pair(ENGINE_KEY_ENOENT, TimingHistogram{});
83+
84+
cb_mutex_enter(&bucket.mutex);
85+
try {
86+
if (bucket.type != BucketType::NoBucket &&
87+
bucket.state == BucketState::Ready && bucketname == bucket.name) {
88+
ret = get_timings(connection, bucket, opcode);
89+
}
90+
} catch (...) {
91+
// we don't want to leave the mutex locked
92+
}
93+
cb_mutex_exit(&bucket.mutex);
94+
95+
return ret;
96+
}
97+
98+
/**
99+
* Get the aggregated timings across "all" buckets that the connected
100+
* client has access to.
101+
*/
102+
static std::pair<ENGINE_ERROR_CODE, std::string> get_aggregated_timings(
103+
McbpConnection& connection, uint8_t opcode) {
104+
TimingHistogram timings;
105+
bool found = false;
106+
107+
for (auto& bucket : all_buckets) {
108+
auto bt = maybe_get_timings(connection, bucket, opcode, bucket.name);
109+
if (bt.first == ENGINE_SUCCESS) {
110+
timings += bt.second;
111+
found = true;
112+
}
113+
}
114+
115+
if (found) {
116+
return std::make_pair(ENGINE_SUCCESS, timings.to_string());
117+
}
118+
119+
// We didn't have access to any buckets!
120+
return std::make_pair(ENGINE_EACCESS, std::string{});
121+
}
122+
24123
std::pair<ENGINE_ERROR_CODE, std::string> get_cmd_timer(
25124
McbpConnection& connection,
26125
const protocol_binary_request_get_cmd_timer* req) {
@@ -30,52 +129,44 @@ std::pair<ENGINE_ERROR_CODE, std::string> get_cmd_timer(
30129
const std::string bucket(key, keylen);
31130
const auto opcode = req->message.body.opcode;
32131

33-
if (keylen > 0 && bucket != all_buckets[index].name) {
34-
// The user specified the current selected bucket
35-
keylen = 0;
132+
if (bucket == "/all/") {
133+
index = 0;
36134
}
37135

38-
if (keylen > 0 || index == 0) {
39-
// You need the Stats privilege in order to specify a bucket
40-
auto ret = mcbp::checkPrivilege(connection, cb::rbac::Privilege::Stats);
41-
if (ret != ENGINE_SUCCESS) {
42-
return std::make_pair(ret, "");
43-
}
136+
if (index == 0) {
137+
return get_aggregated_timings(connection, opcode);
44138
}
45139

46-
// At this point we know that the user have the appropriate access
47-
// and should be permitted to perform the action
48-
if (bucket == "/all/") {
49-
// The aggregated timings is stored in index 0 (no bucket)
50-
index = 0;
51-
keylen = 0;
52-
}
140+
if (bucket.empty() || bucket == all_buckets[index].name) {
141+
// The current selected bucket
142+
auto bt = get_timings(connection, connection.getBucket(), opcode);
143+
if (bt.first == ENGINE_SUCCESS) {
144+
return std::make_pair(ENGINE_SUCCESS, bt.second.to_string());
145+
}
53146

54-
if (keylen == 0) {
55-
return std::make_pair(ENGINE_SUCCESS,
56-
all_buckets[index].timings.generate(opcode));
147+
return std::make_pair(bt.first, std::string{});
57148
}
58149

59150
// The user specified a bucket... let's locate the bucket
60-
std::string str;
151+
std::pair<ENGINE_ERROR_CODE, TimingHistogram> ret;
61152

62-
bool found = false;
63-
for (size_t ii = 1; ii < all_buckets.size() && !found; ++ii) {
64-
// Need the lock to get the bucket state and name
65-
cb_mutex_enter(&all_buckets[ii].mutex);
66-
if ((all_buckets[ii].state == BucketState::Ready) &&
67-
(bucket == all_buckets[ii].name)) {
68-
str = all_buckets[ii].timings.generate(opcode);
69-
found = true;
153+
for (auto& b : all_buckets) {
154+
ret = maybe_get_timings(connection, b, opcode, bucket);
155+
if (ret.first != ENGINE_KEY_ENOENT && ret.first != ENGINE_SUCCESS) {
156+
break;
70157
}
71-
cb_mutex_exit(&all_buckets[ii].mutex);
72158
}
73159

74-
if (found) {
75-
return std::make_pair(ENGINE_SUCCESS, str);
160+
if (ret.first == ENGINE_SUCCESS) {
161+
return std::make_pair(ENGINE_SUCCESS, ret.second.to_string());
162+
}
163+
164+
if (ret.first == ENGINE_KEY_ENOENT) {
165+
// Don't tell the user that the bucket doesn't exist
166+
ret.first = ENGINE_EACCESS;
76167
}
77168

78-
return std::make_pair(ENGINE_KEY_ENOENT, "");
169+
return std::make_pair(ret.first, std::string{});
79170
}
80171

81172
void get_cmd_timer_executor(McbpConnection* c, void* packet) {

daemon/timings.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,13 @@ class Timings {
4646
cb::sampling::Interval get_interval_mutation_latency();
4747
cb::sampling::Interval get_interval_lookup_latency();
4848

49+
/**
50+
* Get the underlying timings histogram for the specified opcode
51+
*/
52+
TimingHistogram get_timing_histogram(uint8_t opcode) const {
53+
return timings[opcode];
54+
}
55+
4956
private:
5057
// This lock is only held by sample() and some blocks within generate().
5158
// It guards the various IntervalSeries variables which internally

programs/mctimings/mctimings.cc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,7 @@ static void request_stat_timings(MemcachedBinprotConnection& connection,
381381
void usage() {
382382
std::cerr << "Usage mctimings [-h host[:port]] [-p port] [-u user]"
383383
<< " [-P pass] [-S passFromStdin] [-b bucket] [-s]"
384-
<< "-v [opcode / stat_name]*" << std::endl
384+
<< " -v [opcode / stat_name]*" << std::endl
385385
<< std::endl
386386
<< "Example:" << std::endl
387387
<< " mctimings -h localhost:11210 -v GET SET"

protocol/connection/client_mcbp_commands.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -899,6 +899,15 @@ using BinprotTouchResponse = BinprotResponse;
899899
class BinprotGetCmdTimerCommand
900900
: public BinprotCommandT<BinprotGetCmdTimerCommand, PROTOCOL_BINARY_CMD_GET_CMD_TIMER> {
901901
public:
902+
BinprotGetCmdTimerCommand() = default;
903+
BinprotGetCmdTimerCommand(uint8_t opcode)
904+
: BinprotCommandT(), opcode(opcode) {
905+
}
906+
BinprotGetCmdTimerCommand(const std::string& bucket, uint8_t opcode)
907+
: BinprotCommandT(), opcode(opcode) {
908+
setKey(bucket);
909+
}
910+
902911
void encode(std::vector<uint8_t>& buf) const override;
903912

904913
void setOpcode(uint8_t opcode) {

tests/testapp/CMakeLists.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ list(APPEND TESTAPP_SOURCES
2222
testapp_cert_tests.cc
2323
testapp_client_test.cc
2424
testapp_client_test.h
25+
testapp_cmd_timers.cc
2526
testapp_dcp.cc
2627
testapp_environment.cc
2728
testapp_environment.h
@@ -250,3 +251,8 @@ ADD_TEST(NAME memcached-regression-tests
250251
WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}
251252
COMMAND memcached_testapp --gtest_filter=TransportProtocols/RegressionTest.*)
252253
SET_TESTS_PROPERTIES(memcached-regression-tests PROPERTIES TIMEOUT 60)
254+
255+
ADD_TEST(NAME memcached-cmd-timers
256+
WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}
257+
COMMAND memcached_testapp --gtest_filter=CmdTimerTest.*)
258+
SET_TESTS_PROPERTIES(memcached-cmd-timers PROPERTIES TIMEOUT 200)

tests/testapp/rbac.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
"buckets": {
2525
"rbac_test": [
2626
"Read",
27-
"XattrRead"
27+
"XattrRead",
28+
"SimpleStats"
2829
]
2930
},
3031
"privileges": [],

0 commit comments

Comments
 (0)