Skip to content

Commit 73a842c

Browse files
committed
Add RBAC message discarding filters
Signed-off-by: Magnus Groß <magnus.gross+github@kdab.com>
1 parent b2e48d7 commit 73a842c

7 files changed

Lines changed: 187 additions & 3 deletions

File tree

src/client/include/Client.hpp

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ class SubscriptionClient : public MDClientBase {
244244
}
245245

246246
void set(const URI<STRICT> & /*uri*/, std::string_view, const std::span<const std::byte> & /*request*/) override {
247-
throw std::logic_error("get not implemented");
247+
throw std::logic_error("set not implemented");
248248
}
249249

250250
void subscribe(const URI<STRICT> &uri, std::string_view /*reqId*/) override {
@@ -399,18 +399,20 @@ class MDClientCtx : public ClientBase {
399399
_requests.erase(0); // todo: lookup correct subscription
400400
}
401401
}
402-
sendCmd(cmd.topic, cmd.command, req_id, cmd.data);
402+
sendCmd(cmd.topic, cmd.command, req_id, cmd.data, cmd.rbac);
403403
}
404404

405405
private:
406-
void sendCmd(const URI<STRICT> &uri, mdp::Command commandType, std::size_t req_id, IoBuffer data = {}) const {
406+
void sendCmd(const URI<STRICT> &uri, mdp::Command commandType, std::size_t req_id, IoBuffer data = {}, IoBuffer rbac = {}) const {
407407
const bool isSet = commandType == mdp::Command::Set;
408408
zmq::MessageFrame cmdType{ std::string{ static_cast<char>(commandType) } };
409409
cmdType.send(_control_socket_send, ZMQ_SNDMORE).assertSuccess();
410410
zmq::MessageFrame reqId{ std::to_string(req_id) };
411411
reqId.send(_control_socket_send, ZMQ_SNDMORE).assertSuccess();
412412
zmq::MessageFrame endpoint{ std::string(uri.str()) };
413413
endpoint.send(_control_socket_send, isSet ? ZMQ_SNDMORE : 0).assertSuccess();
414+
zmq::MessageFrame rbacframe{ std::move(rbac) };
415+
rbacframe.send(_control_socket_send, 0).assertSuccess();
414416
if (isSet) {
415417
zmq::MessageFrame dataframe{ std::move(data) };
416418
dataframe.send(_control_socket_send, 0).assertSuccess();
@@ -421,13 +423,19 @@ class MDClientCtx : public ClientBase {
421423
zmq::MessageFrame cmd;
422424
zmq::MessageFrame reqId;
423425
zmq::MessageFrame endpoint;
426+
zmq::MessageFrame rbac;
424427
while (cmd.receive(_control_socket_recv, ZMQ_DONTWAIT).isValid()) {
425428
if (!reqId.receive(_control_socket_recv, ZMQ_DONTWAIT).isValid()) {
426429
throw std::logic_error("invalid request received: failure receiving message");
427430
}
428431
if (!endpoint.receive(_control_socket_recv, ZMQ_DONTWAIT).isValid()) {
429432
throw std::logic_error("invalid request received: invalid message contents");
430433
}
434+
if (!rbac.receive(_control_socket_recv, ZMQ_DONTWAIT).isValid()) {
435+
throw std::logic_error("invalid request received: invalid rbac data");
436+
}
437+
auto rbacdata = as_bytes(std::span(rbac.data().data(), rbac.data().size()));
438+
IoBuffer rbacData{ reinterpret_cast<const char *>(rbacdata.data()), rbacdata.size() };
431439
URI<STRICT> uri{ std::string(endpoint.data()) };
432440
auto &client = getClient(uri);
433441
if (cmd.data().size() != 1) {

src/majordomo/include/majordomo/Broker.hpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,7 @@ class Broker {
280280
bool _connectedToDns = false;
281281

282282
const IoBuffer _rbac = IoBuffer("RBAC=ADMIN,abcdef12345");
283+
std::function<void(BrokerMessage &)> _rbacHandler;
283284

284285
std::atomic<bool> _shutdownRequested = false;
285286

@@ -422,6 +423,10 @@ class Broker {
422423
_subscriptionMatcher.addFilter<Filter>(key);
423424
}
424425

426+
void setRbacHandler(decltype(_rbacHandler) handler) {
427+
_rbacHandler = std::move(handler);
428+
}
429+
425430
/**
426431
* Bind broker to endpoint, can call this multiple times. We use a single
427432
* socket for both clients and workers.
@@ -855,6 +860,10 @@ class Broker {
855860
auto clientMessage = std::move(client.requests.front());
856861
client.requests.pop_front();
857862

863+
if (_rbacHandler) {
864+
_rbacHandler(clientMessage);
865+
}
866+
858867
if (auto service = bestMatchingService(clientMessage.serviceName)) {
859868
if (service->internalHandler) {
860869
auto reply = service->internalHandler(std::move(clientMessage));

src/majordomo/include/majordomo/Worker.hpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ class BasicWorker {
9898
std::shared_mutex _notificationHandlersLock;
9999
const std::string _notifyAddress;
100100
const IoBuffer _defaultRbacToken = IoBuffer("RBAC=NONE,");
101+
std::function<bool(const IoBuffer &rbac)> _rbacPredicate;
101102

102103
public:
103104
static constexpr std::string_view name = serviceName.data();
@@ -184,6 +185,10 @@ class BasicWorker {
184185
disconnect();
185186
}
186187

188+
void setRbacPredicate(decltype(_rbacPredicate) rbacPredicate) {
189+
_rbacPredicate = std::move(rbacPredicate);
190+
}
191+
187192
protected:
188193
void setHandler(std::function<void(RequestContext &)> handler) {
189194
_handler = std::move(handler);
@@ -352,6 +357,12 @@ class BasicWorker {
352357
const auto clientRole = parse_rbac::role(request.rbac.asString());
353358
const auto permission = _permissionsByRole.at(clientRole, _defaultPermission);
354359

360+
if (_rbacPredicate && !_rbacPredicate(request.rbac)) {
361+
auto errorReply = replyFromRequest(request);
362+
errorReply.error = "request rejected by RBAC handler";
363+
return errorReply;
364+
}
365+
355366
if (request.command == mdp::Command::Get && !(permission == Permission::RW || permission == Permission::RO)) {
356367
auto errorReply = replyFromRequest(request);
357368
errorReply.error = std::format("GET access denied to role '{}'", clientRole);

src/majordomo/test/rbac_tests.cpp

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
#include <Client.hpp>
2+
#include <concepts/majordomo/helpers.hpp>
13
#include <majordomo/Rbac.hpp>
24

35
#include <catch2/catch.hpp>
@@ -30,3 +32,142 @@ TEST_CASE("RBAC parser tests", "[rbac][parsing]") {
3032
STATIC_REQUIRE2(parse_rbac::roleAndHash("RBAC=ADMIN,hash") == std::pair("ADMIN"sv, "hash"sv));
3133
STATIC_REQUIRE2(parse_rbac::hash("RBAC=ADMIN,hash,invalidHash") == "hash,invalidHash"); // TODO in java, this throws, should we throw/return "", too?
3234
}
35+
36+
namespace opencmw {
37+
38+
struct RbacContext {
39+
int dummy;
40+
};
41+
42+
struct RbacInput {
43+
int dummy;
44+
};
45+
46+
struct RbacOutput {
47+
int i;
48+
};
49+
50+
} // namespace opencmw
51+
52+
ENABLE_REFLECTION_FOR(opencmw::RbacContext, dummy)
53+
ENABLE_REFLECTION_FOR(opencmw::RbacInput, dummy)
54+
ENABLE_REFLECTION_FOR(opencmw::RbacOutput, i)
55+
56+
namespace {
57+
using RbacWorkerType = majordomo::Worker<"/rbac", opencmw::RbacContext, opencmw::RbacInput, opencmw::RbacOutput, majordomo::description<"Rbac test">>;
58+
};
59+
60+
class RbacWorker : public RbacWorkerType {
61+
public:
62+
template<typename BrokerType>
63+
explicit RbacWorker(const BrokerType &broker) : RbacWorkerType(broker, {}) { init(); };
64+
65+
private:
66+
void init() {
67+
RbacWorkerType::setCallback([this](const majordomo::RequestContext &rawCtx, const opencmw::RbacContext &context, const opencmw::RbacInput &in, opencmw::RbacContext &replyContext, opencmw::RbacOutput &out) {
68+
out.i = 42;
69+
});
70+
};
71+
};
72+
73+
TEST_CASE("RBAC basic predicate tests", "[rbac][predicate]") {
74+
opencmw::majordomo::Broker<> broker{ "/Broker", {} };
75+
RbacWorker worker{ broker };
76+
77+
worker.setRbacPredicate([](auto &) { return false; });
78+
79+
REQUIRE(broker.bind(opencmw::URI<>("mds://127.0.0.1:12345")));
80+
REQUIRE(broker.bind(opencmw::URI<>("mdp://127.0.0.1:12346")));
81+
82+
RunInThread brokerThread(broker);
83+
RunInThread workerThread(worker);
84+
85+
REQUIRE(waitUntilWorkerServiceAvailable(broker.context, worker));
86+
87+
std::vector<std::unique_ptr<opencmw::client::ClientBase>> clients;
88+
clients.emplace_back(std::make_unique<opencmw::client::MDClientCtx>(broker.context, 20ms, ""));
89+
opencmw::client::ClientContext client{ std::move(clients) };
90+
91+
opencmw::IoBuffer inBuf;
92+
opencmw::RbacInput in;
93+
opencmw::serialise<opencmw::YaS>(inBuf, in);
94+
95+
std::condition_variable cv;
96+
std::mutex m;
97+
bool done = false;
98+
99+
client.set(opencmw::URI("mdp://127.0.0.1:12346/rbac"), [&](const mdp::Message &reply) {
100+
REQUIRE(reply.error.size());
101+
102+
worker.setRbacPredicate([](auto& rb){
103+
return true;});
104+
105+
opencmw::IoBuffer iBuf;
106+
opencmw::serialise<opencmw::YaS>(iBuf, in);
107+
client.set(opencmw::URI("mdp://127.0.0.1:12346/rbac"), [&](const mdp::Message &reply) {
108+
auto outBuf = reply.data;
109+
REQUIRE(reply.error.empty());
110+
REQUIRE(outBuf.size());
111+
opencmw::RbacOutput out;
112+
opencmw::deserialise<opencmw::YaS, opencmw::ProtocolCheck::IGNORE>(outBuf, out);
113+
REQUIRE(out.i == 42);
114+
115+
std::lock_guard lk(m);
116+
done = true;
117+
cv.notify_one();
118+
}, std::move(iBuf)); }, std::move(inBuf));
119+
120+
std::unique_lock lk(m);
121+
cv.wait(lk, [&] { return done; });
122+
}
123+
124+
TEST_CASE("RBAC predicate tests", "[rbac][predicate]") {
125+
opencmw::majordomo::Broker<> broker{ "/Broker", {} };
126+
RbacWorker worker{ broker };
127+
128+
broker.setRbacHandler([](auto &msg) { msg.rbac.put("RBAC=USER,hash"); });
129+
worker.setRbacPredicate([](auto &buf) { return buf.asString().contains("ADMIN"); });
130+
131+
REQUIRE(broker.bind(opencmw::URI<>("mds://127.0.0.1:12345")));
132+
REQUIRE(broker.bind(opencmw::URI<>("mdp://127.0.0.1:12346")));
133+
134+
RunInThread brokerThread(broker);
135+
RunInThread workerThread(worker);
136+
137+
REQUIRE(waitUntilWorkerServiceAvailable(broker.context, worker));
138+
139+
std::vector<std::unique_ptr<opencmw::client::ClientBase>> clients;
140+
clients.emplace_back(std::make_unique<opencmw::client::MDClientCtx>(broker.context, 20ms, ""));
141+
opencmw::client::ClientContext client{ std::move(clients) };
142+
143+
opencmw::IoBuffer inBuf;
144+
opencmw::RbacInput in;
145+
opencmw::serialise<opencmw::YaS>(inBuf, in);
146+
147+
std::condition_variable cv;
148+
std::mutex m;
149+
bool done = false;
150+
151+
client.set(opencmw::URI("mdp://127.0.0.1:12346/rbac"), [&](const mdp::Message &reply) {
152+
REQUIRE(reply.error.size());
153+
154+
broker.setRbacHandler([](auto& msg){msg.rbac.put("RBAC=ADMIN,hash");});
155+
156+
opencmw::IoBuffer iBuf;
157+
opencmw::serialise<opencmw::YaS>(iBuf, in);
158+
client.set(opencmw::URI("mdp://127.0.0.1:12346/rbac"), [&](const mdp::Message &reply) {
159+
auto outBuf = reply.data;
160+
REQUIRE_MESSAGE(reply.error.empty(), reply.error);
161+
REQUIRE(outBuf.size());
162+
opencmw::RbacOutput out;
163+
opencmw::deserialise<opencmw::YaS, opencmw::ProtocolCheck::IGNORE>(outBuf, out);
164+
REQUIRE(out.i == 42);
165+
166+
std::lock_guard lk(m);
167+
done = true;
168+
cv.notify_one();
169+
}, std::move(iBuf)); }, std::move(inBuf));
170+
171+
std::unique_lock lk(m);
172+
cv.wait(lk, [&] { return done; });
173+
}

src/services/include/services/KeyStore.hpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,14 @@ class KeystoreWorker : public KeystoreWorkerType {
5656
_keys[keyHash(roleKey.key)] = roleKey;
5757
}
5858

59+
void addRoles(const std::string &publicKey, const std::string &roles) {
60+
const auto hash = keyHash(publicKey);
61+
if (_keys.contains(hash)) {
62+
auto &key = _keys.at(hash);
63+
key.role = roles;
64+
}
65+
}
66+
5967
private:
6068
void init() {
6169
KeystoreWorkerType::setCallback([this](const majordomo::RequestContext &rawCtx, const KeystoreContext &context, const KeystoreInput &in, KeystoreContext &replyContext, KeystoreOutput &out) {

src/services/include/services/OAuthClient.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,9 @@ class OAuthWorker : public OAuthWorkerType {
324324

325325
// get RBAC roles
326326
out.roles = _client.getAssignedRoles(out.accessToken);
327+
if (!in.publicKey.empty()) {
328+
_keystore.addRoles(in.publicKey, out.roles);
329+
}
327330
}
328331
});
329332
_keystoreThread = std::thread([this] { _keystore.run(); });

src/services/test/OAuthClient_tests.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
#include <Client.hpp>
88
#include <concepts/majordomo/helpers.hpp>
9+
#include <Debug.hpp>
910
#include <majordomo/Rbac.hpp>
1011
#include <services/OAuthClient.hpp>
1112

@@ -128,6 +129,7 @@ TEST_CASE("Worker test", "[OAuth]") {
128129
client.set(opencmw::URI("mdp://127.0.0.1:12346/oauth"), [&](const mdp::Message &reply) {
129130
opencmw::OAuthOutput out;
130131
auto outBuf = reply.data;
132+
REQUIRE_MESSAGE(reply.error.empty(), reply.error);
131133
opencmw::deserialise<opencmw::YaS, opencmw::ProtocolCheck::IGNORE>(outBuf, out);
132134

133135
REQUIRE(out.authorizationUri.size());
@@ -140,6 +142,7 @@ TEST_CASE("Worker test", "[OAuth]") {
140142
client.set(opencmw::URI("mdp://127.0.0.1:12346/oauth"), [&](const mdp::Message& rep) {
141143
opencmw::OAuthOutput tokenOut;
142144
auto tokenOutBuf = rep.data;
145+
REQUIRE_MESSAGE(rep.error.empty(), rep.error);
143146
opencmw::deserialise<opencmw::YaS, opencmw::ProtocolCheck::IGNORE>(tokenOutBuf, tokenOut);
144147

145148
// we must have got an access token now
@@ -154,6 +157,7 @@ TEST_CASE("Worker test", "[OAuth]") {
154157
client.set(opencmw::URI("mdp://127.0.0.1:12346/keystore"), [&](const mdp::Message &keyResp) {
155158
opencmw::KeystoreOutput keyOut;
156159
auto keyOutBuf = keyResp.data;
160+
REQUIRE_MESSAGE(keyResp.error.empty(), keyResp.error);
157161
opencmw::deserialise<opencmw::YaS, opencmw::ProtocolCheck::IGNORE>(keyOutBuf, keyOut);
158162

159163
// the hashes should be the same

0 commit comments

Comments
 (0)