summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--fnet/src/tests/frt/rpc/invoke.cpp55
-rw-r--r--fnet/src/tests/info/info.cpp2
-rw-r--r--fnet/src/vespa/fnet/connection.cpp10
-rw-r--r--fnet/src/vespa/fnet/connection.h14
-rw-r--r--fnet/src/vespa/fnet/frt/CMakeLists.txt1
-rw-r--r--fnet/src/vespa/fnet/frt/error.cpp54
-rw-r--r--fnet/src/vespa/fnet/frt/error.h31
-rw-r--r--fnet/src/vespa/fnet/frt/invoker.cpp6
-rw-r--r--fnet/src/vespa/fnet/frt/reflection.cpp17
-rw-r--r--fnet/src/vespa/fnet/frt/reflection.h9
-rw-r--r--fnet/src/vespa/fnet/frt/request_access_filter.h24
-rw-r--r--fnet/src/vespa/fnet/frt/require_capability.cpp13
-rw-r--r--fnet/src/vespa/fnet/frt/require_capability.h21
-rw-r--r--vespalib/CMakeLists.txt1
-rw-r--r--vespalib/src/tests/net/tls/capabilities/CMakeLists.txt10
-rw-r--r--vespalib/src/tests/net/tls/capabilities/capabilities_test.cpp152
-rw-r--r--vespalib/src/tests/net/tls/openssl_impl/openssl_impl_test.cpp27
-rw-r--r--vespalib/src/tests/net/tls/policy_checking_certificate_verifier/policy_checking_certificate_verifier_test.cpp111
-rw-r--r--vespalib/src/tests/net/tls/transport_options/transport_options_reading_test.cpp62
-rw-r--r--vespalib/src/vespa/vespalib/net/CMakeLists.txt1
-rw-r--r--vespalib/src/vespa/vespalib/net/connection_auth_context.cpp21
-rw-r--r--vespalib/src/vespa/vespalib/net/connection_auth_context.h29
-rw-r--r--vespalib/src/vespa/vespalib/net/crypto_socket.cpp9
-rw-r--r--vespalib/src/vespa/vespalib/net/crypto_socket.h14
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/CMakeLists.txt3
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/assumed_roles.cpp95
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/assumed_roles.h80
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/capability.cpp62
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/capability.h109
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/capability_set.cpp95
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/capability_set.h104
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/certificate_verification_callback.h2
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/crypto_codec.h6
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/crypto_codec_adapter.cpp7
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/crypto_codec_adapter.h1
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/impl/openssl_crypto_codec_impl.h12
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/impl/openssl_tls_context_impl.cpp2
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/peer_policies.cpp11
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/peer_policies.h14
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/policy_checking_certificate_verifier.cpp10
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/tls_crypto_socket.h2
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/transport_security_options_reading.cpp34
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/verification_result.cpp14
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/verification_result.h25
-rw-r--r--vespalib/src/vespa/vespalib/test/peer_policy_utils.cpp26
-rw-r--r--vespalib/src/vespa/vespalib/test/peer_policy_utils.h9
46 files changed, 1055 insertions, 362 deletions
diff --git a/fnet/src/tests/frt/rpc/invoke.cpp b/fnet/src/tests/frt/rpc/invoke.cpp
index 1fbd356b239..e1912985379 100644
--- a/fnet/src/tests/frt/rpc/invoke.cpp
+++ b/fnet/src/tests/frt/rpc/invoke.cpp
@@ -7,8 +7,10 @@
#include <vespa/fnet/frt/target.h>
#include <vespa/fnet/frt/rpcrequest.h>
#include <vespa/fnet/frt/invoker.h>
+#include <vespa/fnet/frt/request_access_filter.h>
#include <mutex>
#include <condition_variable>
+#include <string_view>
using vespalib::SocketSpec;
using vespalib::BenchmarkTimer;
@@ -175,11 +177,25 @@ public:
//-------------------------------------------------------------
+struct MyAccessFilter : FRT_RequestAccessFilter {
+ ~MyAccessFilter() override = default;
+
+ constexpr static std::string_view WRONG_KEY = "...mellon!";
+ constexpr static std::string_view CORRECT_KEY = "let me in, I have cake";
+
+ bool allow(FRT_RPCRequest& req) const noexcept override {
+ const auto& req_param = req.GetParams()->GetValue(0)._string;
+ const auto magic_key = std::string_view(req_param._str, req_param._len);
+ return (magic_key == CORRECT_KEY);
+ }
+};
+
class TestRPC : public FRT_Invokable
{
private:
- uint32_t _intValue;
- RequestLatch _detached_req;
+ uint32_t _intValue;
+ RequestLatch _detached_req;
+ std::atomic<bool> _restricted_method_was_invoked;
TestRPC(const TestRPC &);
TestRPC &operator=(const TestRPC &);
@@ -187,7 +203,8 @@ private:
public:
TestRPC(FRT_Supervisor *supervisor)
: _intValue(0),
- _detached_req()
+ _detached_req(),
+ _restricted_method_was_invoked(false)
{
FRT_ReflectionBuilder rb(supervisor);
@@ -201,6 +218,9 @@ public:
FRT_METHOD(TestRPC::RPC_GetValue), this);
rb.DefineMethod("test", "iibb", "i",
FRT_METHOD(TestRPC::RPC_Test), this);
+ rb.DefineMethod("accessRestricted", "s", "",
+ FRT_METHOD(TestRPC::RPC_AccessRestricted), this);
+ rb.RequestAccessFilter(std::make_unique<MyAccessFilter>());
}
void RPC_Test(FRT_RPCRequest *req)
@@ -244,6 +264,16 @@ public:
req->GetReturn()->AddInt32(_intValue);
}
+ void RPC_AccessRestricted([[maybe_unused]] FRT_RPCRequest *req)
+ {
+ // We'll only get here if the access filter lets us in
+ _restricted_method_was_invoked.store(true);
+ }
+
+ bool restricted_method_was_invoked() const noexcept {
+ return _restricted_method_was_invoked.load();
+ }
+
RequestLatch &detached_req() { return _detached_req; }
};
@@ -264,6 +294,7 @@ public:
FRT_Target *make_bad_target() { return _client.supervisor().GetTarget("bogus address"); }
RequestLatch &detached_req() { return _testRPC.detached_req(); }
EchoTest &echo() { return _echoTest; }
+ const TestRPC& server_instance() const noexcept { return _testRPC; }
Fixture()
: _client(crypto),
@@ -421,6 +452,24 @@ TEST_F("require that parameters can be echoed as return values", Fixture()) {
EXPECT_TRUE(req.get().GetParams()->Equals(req.get().GetReturn()));
}
+TEST_F("request denied by access filter returns PERMISSION_DENIED and does not invoke server method", Fixture()) {
+ MyReq req("accessRestricted");
+ auto key = MyAccessFilter::WRONG_KEY;
+ req.get().GetParams()->AddString(key.data(), key.size());
+ f1.target().InvokeSync(req.borrow(), timeout);
+ EXPECT_EQUAL(req.get().GetErrorCode(), FRTE_RPC_PERMISSION_DENIED);
+ EXPECT_FALSE(f1.server_instance().restricted_method_was_invoked());
+}
+
+TEST_F("request allowed by access filter invokes server method as usual", Fixture()) {
+ MyReq req("accessRestricted");
+ auto key = MyAccessFilter::CORRECT_KEY;
+ req.get().GetParams()->AddString(key.data(), key.size());
+ f1.target().InvokeSync(req.borrow(), timeout);
+ ASSERT_FALSE(req.get().IsError());
+ EXPECT_TRUE(f1.server_instance().restricted_method_was_invoked());
+}
+
TEST_MAIN() {
crypto = my_crypto_engine();
TEST_RUN_ALL();
diff --git a/fnet/src/tests/info/info.cpp b/fnet/src/tests/info/info.cpp
index 0d4e0f90a09..4271546e647 100644
--- a/fnet/src/tests/info/info.cpp
+++ b/fnet/src/tests/info/info.cpp
@@ -80,7 +80,7 @@ TEST("size of important objects")
EXPECT_EQUAL(MUTEX_SIZE + sizeof(std::string) + 112u, sizeof(FNET_IOComponent));
EXPECT_EQUAL(32u, sizeof(FNET_Channel));
EXPECT_EQUAL(40u, sizeof(FNET_PacketQueue_NoLock));
- EXPECT_EQUAL(MUTEX_SIZE + sizeof(std::string) + 408u, sizeof(FNET_Connection));
+ EXPECT_EQUAL(MUTEX_SIZE + sizeof(std::string) + 416u, sizeof(FNET_Connection));
EXPECT_EQUAL(48u, sizeof(std::condition_variable));
EXPECT_EQUAL(56u, sizeof(FNET_DataBuffer));
EXPECT_EQUAL(8u, sizeof(FNET_Context));
diff --git a/fnet/src/vespa/fnet/connection.cpp b/fnet/src/vespa/fnet/connection.cpp
index 2677445e35d..26367c904b2 100644
--- a/fnet/src/vespa/fnet/connection.cpp
+++ b/fnet/src/vespa/fnet/connection.cpp
@@ -9,6 +9,7 @@
#include "config.h"
#include "transport_thread.h"
#include "transport.h"
+#include <vespa/vespalib/net/connection_auth_context.h>
#include <vespa/vespalib/net/socket_spec.h>
#include <vespa/log/log.h>
@@ -241,6 +242,8 @@ FNET_Connection::handshake()
break;
case vespalib::CryptoSocket::HandshakeResult::DONE: {
LOG(debug, "Connection(%s): handshake done with peer %s", GetSpec(), GetPeerSpec().c_str());
+ _auth_context = _socket->make_auth_context();
+ assert(_auth_context);
EnableReadEvent(true);
EnableWriteEvent(writePendingAfterConnect());
_flags._framed = (_socket->min_read_buffer_size() > 1);
@@ -764,3 +767,10 @@ FNET_Connection::GetPeerSpec() const
{
return vespalib::SocketAddress::peer_address(_socket->get_fd()).spec();
}
+
+const vespalib::net::ConnectionAuthContext&
+FNET_Connection::auth_context() const noexcept
+{
+ assert(_auth_context);
+ return *_auth_context;
+}
diff --git a/fnet/src/vespa/fnet/connection.h b/fnet/src/vespa/fnet/connection.h
index 15150ffbb07..4d66f22ce2b 100644
--- a/fnet/src/vespa/fnet/connection.h
+++ b/fnet/src/vespa/fnet/connection.h
@@ -18,6 +18,8 @@ class FNET_IPacketStreamer;
class FNET_IServerAdapter;
class FNET_IPacketHandler;
+namespace vespalib::net { class ConnectionAuthContext; }
+
/**
* Interface implemented by objects that want to perform connection
* cleanup. Use the SetCleanupHandler method to register with a
@@ -96,7 +98,7 @@ private:
using ResolveHandlerSP = std::shared_ptr<ResolveHandler>;
FNET_IPacketStreamer *_streamer; // custom packet streamer
FNET_IServerAdapter *_serverAdapter; // only on server side
- vespalib::CryptoSocket::UP _socket; // socket for this conn
+ vespalib::CryptoSocket::UP _socket; // socket for this conn
ResolveHandlerSP _resolve_handler; // async resolve callback
FNET_Context _context; // connection context
std::atomic<State> _state; // connection state. May be polled outside lock
@@ -115,6 +117,8 @@ private:
FNET_IConnectionCleanupHandler *_cleanup; // cleanup handler
+ std::unique_ptr<vespalib::net::ConnectionAuthContext> _auth_context;
+
static std::atomic<uint64_t> _num_connections; // total number of connections
@@ -277,7 +281,7 @@ public:
/**
* Destructor.
**/
- ~FNET_Connection();
+ ~FNET_Connection() override;
/**
@@ -504,6 +508,12 @@ public:
uint32_t getInputBufferSize() const { return _input.GetBufSize(); }
/**
+ * Returns the connection's auth context. Must only be called _after_ the
+ * handshake phase has completed.
+ */
+ const vespalib::net::ConnectionAuthContext& auth_context() const noexcept;
+
+ /**
* @return the total number of connection objects
**/
static uint64_t get_num_connections() {
diff --git a/fnet/src/vespa/fnet/frt/CMakeLists.txt b/fnet/src/vespa/fnet/frt/CMakeLists.txt
index c7bcbe27041..fa9623b950a 100644
--- a/fnet/src/vespa/fnet/frt/CMakeLists.txt
+++ b/fnet/src/vespa/fnet/frt/CMakeLists.txt
@@ -5,6 +5,7 @@ vespa_add_library(fnet_frt OBJECT
invoker.cpp
packets.cpp
reflection.cpp
+ require_capability.cpp
rpcrequest.cpp
supervisor.cpp
target.cpp
diff --git a/fnet/src/vespa/fnet/frt/error.cpp b/fnet/src/vespa/fnet/frt/error.cpp
index 6af9ea39757..fb91924bf35 100644
--- a/fnet/src/vespa/fnet/frt/error.cpp
+++ b/fnet/src/vespa/fnet/frt/error.cpp
@@ -12,19 +12,20 @@ FRT_GetErrorCodeName(uint32_t errorCode)
errorCode <= FRTE_RPC_LAST)
{
switch (errorCode) {
- case FRTE_RPC_GENERAL_ERROR: return "FRTE_RPC_GENERAL_ERROR";
- case FRTE_RPC_NOT_IMPLEMENTED: return "FRTE_RPC_NOT_IMPLEMENTED";
- case FRTE_RPC_ABORT: return "FRTE_RPC_ABORT";
- case FRTE_RPC_TIMEOUT: return "FRTE_RPC_TIMEOUT";
- case FRTE_RPC_CONNECTION: return "FRTE_RPC_CONNECTION";
- case FRTE_RPC_BAD_REQUEST: return "FRTE_RPC_BAD_REQUEST";
- case FRTE_RPC_NO_SUCH_METHOD: return "FRTE_RPC_NO_SUCH_METHOD";
- case FRTE_RPC_WRONG_PARAMS: return "FRTE_RPC_WRONG_PARAMS";
- case FRTE_RPC_OVERLOAD: return "FRTE_RPC_OVERLOAD";
- case FRTE_RPC_WRONG_RETURN: return "FRTE_RPC_WRONG_RETURN";
- case FRTE_RPC_BAD_REPLY: return "FRTE_RPC_BAD_REPLY";
- case FRTE_RPC_METHOD_FAILED: return "FRTE_RPC_METHOD_FAILED";
- default: return "[UNKNOWN RPC ERROR]";
+ case FRTE_RPC_GENERAL_ERROR: return "FRTE_RPC_GENERAL_ERROR";
+ case FRTE_RPC_NOT_IMPLEMENTED: return "FRTE_RPC_NOT_IMPLEMENTED";
+ case FRTE_RPC_ABORT: return "FRTE_RPC_ABORT";
+ case FRTE_RPC_TIMEOUT: return "FRTE_RPC_TIMEOUT";
+ case FRTE_RPC_CONNECTION: return "FRTE_RPC_CONNECTION";
+ case FRTE_RPC_BAD_REQUEST: return "FRTE_RPC_BAD_REQUEST";
+ case FRTE_RPC_NO_SUCH_METHOD: return "FRTE_RPC_NO_SUCH_METHOD";
+ case FRTE_RPC_WRONG_PARAMS: return "FRTE_RPC_WRONG_PARAMS";
+ case FRTE_RPC_OVERLOAD: return "FRTE_RPC_OVERLOAD";
+ case FRTE_RPC_WRONG_RETURN: return "FRTE_RPC_WRONG_RETURN";
+ case FRTE_RPC_BAD_REPLY: return "FRTE_RPC_BAD_REPLY";
+ case FRTE_RPC_METHOD_FAILED: return "FRTE_RPC_METHOD_FAILED";
+ case FRTE_RPC_PERMISSION_DENIED: return "FRTE_RPC_PERMISSION_DENIED";
+ default: return "[UNKNOWN RPC ERROR]";
}
}
return "[UNKNOWN ERROR]";
@@ -41,19 +42,20 @@ FRT_GetDefaultErrorMessage(uint32_t errorCode)
errorCode <= FRTE_RPC_LAST)
{
switch (errorCode) {
- case FRTE_RPC_GENERAL_ERROR: return "(RPC) General error";
- case FRTE_RPC_NOT_IMPLEMENTED: return "(RPC) Not implemented";
- case FRTE_RPC_ABORT: return "(RPC) Invocation aborted";
- case FRTE_RPC_TIMEOUT: return "(RPC) Invocation timed out";
- case FRTE_RPC_CONNECTION: return "(RPC) Connection error";
- case FRTE_RPC_BAD_REQUEST: return "(RPC) Bad request packet";
- case FRTE_RPC_NO_SUCH_METHOD: return "(RPC) No such method";
- case FRTE_RPC_WRONG_PARAMS: return "(RPC) Illegal parameters";
- case FRTE_RPC_OVERLOAD: return "(RPC) Request dropped due to server overload";
- case FRTE_RPC_WRONG_RETURN: return "(RPC) Illegal return values";
- case FRTE_RPC_BAD_REPLY: return "(RPC) Bad reply packet";
- case FRTE_RPC_METHOD_FAILED: return "(RPC) Method failed";
- default: return "[UNKNOWN RPC ERROR]";
+ case FRTE_RPC_GENERAL_ERROR: return "(RPC) General error";
+ case FRTE_RPC_NOT_IMPLEMENTED: return "(RPC) Not implemented";
+ case FRTE_RPC_ABORT: return "(RPC) Invocation aborted";
+ case FRTE_RPC_TIMEOUT: return "(RPC) Invocation timed out";
+ case FRTE_RPC_CONNECTION: return "(RPC) Connection error";
+ case FRTE_RPC_BAD_REQUEST: return "(RPC) Bad request packet";
+ case FRTE_RPC_NO_SUCH_METHOD: return "(RPC) No such method";
+ case FRTE_RPC_WRONG_PARAMS: return "(RPC) Illegal parameters";
+ case FRTE_RPC_OVERLOAD: return "(RPC) Request dropped due to server overload";
+ case FRTE_RPC_WRONG_RETURN: return "(RPC) Illegal return values";
+ case FRTE_RPC_BAD_REPLY: return "(RPC) Bad reply packet";
+ case FRTE_RPC_METHOD_FAILED: return "(RPC) Method failed";
+ case FRTE_RPC_PERMISSION_DENIED: return "(RPC) Permission denied";
+ default: return "[UNKNOWN RPC ERROR]";
}
}
return "[UNKNOWN ERROR]";
diff --git a/fnet/src/vespa/fnet/frt/error.h b/fnet/src/vespa/fnet/frt/error.h
index c5acfb744f6..7b3cdc7320b 100644
--- a/fnet/src/vespa/fnet/frt/error.h
+++ b/fnet/src/vespa/fnet/frt/error.h
@@ -4,21 +4,22 @@
#include <cstdint>
enum {
- FRTE_NO_ERROR = 0,
- FRTE_RPC_FIRST = 100,
- FRTE_RPC_GENERAL_ERROR = 100,
- FRTE_RPC_NOT_IMPLEMENTED = 101,
- FRTE_RPC_ABORT = 102,
- FRTE_RPC_TIMEOUT = 103,
- FRTE_RPC_CONNECTION = 104,
- FRTE_RPC_BAD_REQUEST = 105,
- FRTE_RPC_NO_SUCH_METHOD = 106,
- FRTE_RPC_WRONG_PARAMS = 107,
- FRTE_RPC_OVERLOAD = 108,
- FRTE_RPC_WRONG_RETURN = 109,
- FRTE_RPC_BAD_REPLY = 110,
- FRTE_RPC_METHOD_FAILED = 111,
- FRTE_RPC_LAST = 199
+ FRTE_NO_ERROR = 0,
+ FRTE_RPC_FIRST = 100,
+ FRTE_RPC_GENERAL_ERROR = 100,
+ FRTE_RPC_NOT_IMPLEMENTED = 101,
+ FRTE_RPC_ABORT = 102,
+ FRTE_RPC_TIMEOUT = 103,
+ FRTE_RPC_CONNECTION = 104,
+ FRTE_RPC_BAD_REQUEST = 105,
+ FRTE_RPC_NO_SUCH_METHOD = 106,
+ FRTE_RPC_WRONG_PARAMS = 107,
+ FRTE_RPC_OVERLOAD = 108,
+ FRTE_RPC_WRONG_RETURN = 109,
+ FRTE_RPC_BAD_REPLY = 110,
+ FRTE_RPC_METHOD_FAILED = 111,
+ FRTE_RPC_PERMISSION_DENIED = 112,
+ FRTE_RPC_LAST = 199
};
const char *FRT_GetErrorCodeName(uint32_t errorCode);
diff --git a/fnet/src/vespa/fnet/frt/invoker.cpp b/fnet/src/vespa/fnet/frt/invoker.cpp
index 85eae6cb41a..f75526d51f1 100644
--- a/fnet/src/vespa/fnet/frt/invoker.cpp
+++ b/fnet/src/vespa/fnet/frt/invoker.cpp
@@ -52,6 +52,7 @@ FRT_RPCInvoker::FRT_RPCInvoker(FRT_Supervisor *supervisor,
std::string methodName(_req->GetMethodName(), _req->GetMethodNameLen());
LOG(debug, "invoke(server) init: '%s'", methodName.c_str());
}
+ req->SetReturnHandler(this); // Must be set prior to any access filter being invoked
if (_method == nullptr) {
if (!req->IsError()) { // may be BAD_REQUEST
req->SetError(FRTE_RPC_NO_SUCH_METHOD);
@@ -60,8 +61,11 @@ FRT_RPCInvoker::FRT_RPCInvoker(FRT_Supervisor *supervisor,
req->GetParamSpec()))
{
req->SetError(FRTE_RPC_WRONG_PARAMS);
+ } else if (_method->GetRequestAccessFilter() &&
+ !_method->GetRequestAccessFilter()->allow(*req))
+ {
+ req->SetError(FRTE_RPC_PERMISSION_DENIED);
}
- req->SetReturnHandler(this);
}
bool FRT_RPCInvoker::Invoke()
diff --git a/fnet/src/vespa/fnet/frt/reflection.cpp b/fnet/src/vespa/fnet/frt/reflection.cpp
index 211e681df94..af7fa069eb9 100644
--- a/fnet/src/vespa/fnet/frt/reflection.cpp
+++ b/fnet/src/vespa/fnet/frt/reflection.cpp
@@ -14,7 +14,8 @@ FRT_Method::FRT_Method(const char * name, const char * paramSpec, const char * r
_returnSpec(returnSpec),
_method(method),
_handler(handler),
- _doc()
+ _doc(),
+ _access_filter()
{
}
@@ -124,6 +125,7 @@ FRT_ReflectionBuilder::Flush()
}
_method->SetDocumentation(_values);
+ _method->SetRequestAccessFilter(std::move(_access_filter)); // May be nullptr
_method = nullptr;
_req->Reset();
}
@@ -142,7 +144,8 @@ FRT_ReflectionBuilder::FRT_ReflectionBuilder(FRT_Supervisor *supervisor)
_arg_name(nullptr),
_arg_desc(nullptr),
_ret_name(nullptr),
- _ret_desc(nullptr)
+ _ret_desc(nullptr),
+ _access_filter()
{
}
@@ -183,6 +186,7 @@ FRT_ReflectionBuilder::DefineMethod(const char *name,
_arg_desc = _values->AddStringArray(_argCnt);
_ret_name = _values->AddStringArray(_retCnt);
_ret_desc = _values->AddStringArray(_retCnt);
+ _access_filter.reset();
}
@@ -224,3 +228,12 @@ FRT_ReflectionBuilder::ReturnDesc(const char *name, const char *desc)
_values->SetString(&_ret_desc[_curRet], desc);
_curRet++;
}
+
+void
+FRT_ReflectionBuilder::RequestAccessFilter(std::unique_ptr<FRT_RequestAccessFilter> access_filter)
+{
+ if (_method == nullptr) {
+ return;
+ }
+ _access_filter = std::move(access_filter);
+}
diff --git a/fnet/src/vespa/fnet/frt/reflection.h b/fnet/src/vespa/fnet/frt/reflection.h
index 6267cafeeb1..3f833d053f1 100644
--- a/fnet/src/vespa/fnet/frt/reflection.h
+++ b/fnet/src/vespa/fnet/frt/reflection.h
@@ -3,6 +3,8 @@
#pragma once
#include "invokable.h"
+#include "request_access_filter.h"
+#include <memory>
#include <string>
#include <vector>
@@ -23,6 +25,7 @@ private:
FRT_METHOD_PT _method; // method pointer
FRT_Invokable *_handler; // method handler
std::vector<char> _doc; // method documentation
+ std::unique_ptr<FRT_RequestAccessFilter> _access_filter; // (optional) access filter
public:
FRT_Method(const FRT_Method &) = delete;
@@ -41,6 +44,10 @@ public:
const char *GetReturnSpec() { return _returnSpec.c_str(); }
FRT_METHOD_PT GetMethod() { return _method; }
FRT_Invokable *GetHandler() { return _handler; }
+ const FRT_RequestAccessFilter* GetRequestAccessFilter() const noexcept { return _access_filter.get(); }
+ void SetRequestAccessFilter(std::unique_ptr<FRT_RequestAccessFilter> access_filter) noexcept {
+ _access_filter = std::move(access_filter);
+ }
void SetDocumentation(FRT_Values *values);
void GetDocumentation(FRT_Values *values);
};
@@ -104,6 +111,7 @@ private:
FRT_StringValue *_arg_desc;
FRT_StringValue *_ret_name;
FRT_StringValue *_ret_desc;
+ std::unique_ptr<FRT_RequestAccessFilter> _access_filter;
FRT_ReflectionBuilder(const FRT_ReflectionBuilder &);
FRT_ReflectionBuilder &operator=(const FRT_ReflectionBuilder &);
@@ -122,5 +130,6 @@ public:
void MethodDesc(const char *desc);
void ParamDesc(const char *name, const char *desc);
void ReturnDesc(const char *name, const char *desc);
+ void RequestAccessFilter(std::unique_ptr<FRT_RequestAccessFilter> access_filter);
};
diff --git a/fnet/src/vespa/fnet/frt/request_access_filter.h b/fnet/src/vespa/fnet/frt/request_access_filter.h
new file mode 100644
index 00000000000..a02dca646f3
--- /dev/null
+++ b/fnet/src/vespa/fnet/frt/request_access_filter.h
@@ -0,0 +1,24 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+#pragma once
+
+class FRT_RPCRequest;
+
+/**
+ * An RPC request access filter will, if provided during method registration, be
+ * invoked _prior_ to any RPC handler callback invocation for that method. It allows
+ * for implementing method-specific authorization handling, logging etc.
+ *
+ * Must be thread safe.
+ */
+class FRT_RequestAccessFilter {
+public:
+ virtual ~FRT_RequestAccessFilter() = default;
+
+ /**
+ * Iff true is returned, the request is allowed through and the RPC callback
+ * will be invoked as usual. If false, the request is immediately failed back
+ * to the caller with an error code.
+ */
+ [[nodiscard]] virtual bool allow(FRT_RPCRequest&) const noexcept = 0;
+};
diff --git a/fnet/src/vespa/fnet/frt/require_capability.cpp b/fnet/src/vespa/fnet/frt/require_capability.cpp
new file mode 100644
index 00000000000..5c64c2bb123
--- /dev/null
+++ b/fnet/src/vespa/fnet/frt/require_capability.cpp
@@ -0,0 +1,13 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+#include "require_capability.h"
+#include "rpcrequest.h"
+#include <vespa/fnet/connection.h>
+#include <vespa/vespalib/net/connection_auth_context.h>
+
+bool
+FRT_RequireCapability::allow(FRT_RPCRequest& req) const noexcept
+{
+ const auto& auth_ctx = req.GetConnection()->auth_context();
+ return auth_ctx.capabilities().contains_all(_required_capabilities);
+}
diff --git a/fnet/src/vespa/fnet/frt/require_capability.h b/fnet/src/vespa/fnet/frt/require_capability.h
new file mode 100644
index 00000000000..c9eaf4937a8
--- /dev/null
+++ b/fnet/src/vespa/fnet/frt/require_capability.h
@@ -0,0 +1,21 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+#pragma once
+
+#include "request_access_filter.h"
+#include <vespa/vespalib/net/tls/capability_set.h>
+
+/**
+ * An RPC access filter which verifies that a request is associated with an auth
+ * context that contains, at minimum, a given set of capabilities. If one or more
+ * required capabilities are missing, the request is denied.
+ */
+class FRT_RequireCapability final : public FRT_RequestAccessFilter {
+ vespalib::net::tls::CapabilitySet _required_capabilities;
+public:
+ explicit constexpr FRT_RequireCapability(vespalib::net::tls::CapabilitySet required_capabilities) noexcept
+ : _required_capabilities(required_capabilities)
+ {
+ }
+
+ bool allow(FRT_RPCRequest& req) const noexcept override;
+};
diff --git a/vespalib/CMakeLists.txt b/vespalib/CMakeLists.txt
index 69bd709c613..609c825dafa 100644
--- a/vespalib/CMakeLists.txt
+++ b/vespalib/CMakeLists.txt
@@ -101,6 +101,7 @@ vespa_define_module(
src/tests/net/socket_spec
src/tests/net/sync_crypto_socket
src/tests/net/tls/auto_reloading_tls_crypto_engine
+ src/tests/net/tls/capabilities
src/tests/net/tls/direct_buffer_bio
src/tests/net/tls/openssl_impl
src/tests/net/tls/policy_checking_certificate_verifier
diff --git a/vespalib/src/tests/net/tls/capabilities/CMakeLists.txt b/vespalib/src/tests/net/tls/capabilities/CMakeLists.txt
new file mode 100644
index 00000000000..4e366674d36
--- /dev/null
+++ b/vespalib/src/tests/net/tls/capabilities/CMakeLists.txt
@@ -0,0 +1,10 @@
+# Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+vespa_add_executable(vespalib_net_tls_capabilities_test_app TEST
+ SOURCES
+ capabilities_test.cpp
+ DEPENDS
+ vespalib
+)
+vespa_add_test(NAME vespalib_net_tls_capabilities_test_app
+ COMMAND vespalib_net_tls_capabilities_test_app)
+
diff --git a/vespalib/src/tests/net/tls/capabilities/capabilities_test.cpp b/vespalib/src/tests/net/tls/capabilities/capabilities_test.cpp
new file mode 100644
index 00000000000..5f74bdceff4
--- /dev/null
+++ b/vespalib/src/tests/net/tls/capabilities/capabilities_test.cpp
@@ -0,0 +1,152 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+#include <vespa/vespalib/net/tls/capability_set.h>
+#include <vespa/vespalib/testkit/test_kit.h>
+
+using namespace vespalib;
+using namespace vespalib::net::tls;
+using namespace std::string_view_literals;
+
+TEST("Capability bit positions are stable across calls") {
+ auto cap1 = Capability::content_storage_api();
+ auto cap2 = Capability::content_storage_api();
+ EXPECT_EQUAL(cap1.id_bit_pos(), cap2.id_bit_pos());
+}
+
+TEST("Capability instances are equality comparable") {
+ auto cap1 = Capability::content_document_api();
+ auto cap2 = Capability::content_document_api();
+ auto cap3 = Capability::content_storage_api();
+ EXPECT_EQUAL(cap1, cap2);
+ EXPECT_EQUAL(cap2, cap1);
+ EXPECT_NOT_EQUAL(cap1, cap3);
+}
+
+TEST("Can get underlying name of all Capability instances") {
+ EXPECT_EQUAL(Capability::content_storage_api().name(), "vespa.content.storage_api"sv);
+ EXPECT_EQUAL(Capability::content_document_api().name(), "vespa.content.document_api"sv);
+ EXPECT_EQUAL(Capability::content_search_api().name(), "vespa.content.search_api"sv);
+ EXPECT_EQUAL(Capability::slobrok_api().name(), "vespa.slobrok.api"sv);
+ EXPECT_EQUAL(Capability::content_status_pages().name(), "vespa.content.status_pages"sv);
+ EXPECT_EQUAL(Capability::content_metrics_api().name(), "vespa.content.metrics_api"sv);
+ EXPECT_EQUAL(Capability::content_cluster_controller_internal_state_api().name(),
+ "vespa.content.cluster_controller.internal_state_api"sv);
+}
+
+TEST("Capability instances can be stringified") {
+ EXPECT_EQUAL(Capability::content_storage_api().to_string(), "Capability(vespa.content.storage_api)");
+}
+
+TEST("All known capabilities can be looked up by name") {
+ EXPECT_TRUE(Capability::find_capability("vespa.content.storage_api").has_value());
+ EXPECT_TRUE(Capability::find_capability("vespa.content.document_api").has_value());
+ EXPECT_TRUE(Capability::find_capability("vespa.content.search_api").has_value());
+ EXPECT_TRUE(Capability::find_capability("vespa.content.cluster_controller.internal_state_api").has_value());
+ EXPECT_TRUE(Capability::find_capability("vespa.slobrok.api").has_value());
+ EXPECT_TRUE(Capability::find_capability("vespa.content.status_pages").has_value());
+ EXPECT_TRUE(Capability::find_capability("vespa.content.metrics_api").has_value());
+}
+
+TEST("Unknown capability name returns nullopt") {
+ EXPECT_FALSE(Capability::find_capability("vespa.content.stale_cat_memes").has_value());
+}
+
+TEST("CapabilitySet instances can be stringified") {
+ EXPECT_EQUAL(CapabilitySet::content_node().to_string(),
+ "CapabilitySet({vespa.content.storage_api, vespa.content.document_api, vespa.slobrok.api})");
+}
+
+TEST("All known capability sets can be looked up by name") {
+ EXPECT_TRUE(CapabilitySet::find_capability_set("vespa.content_node").has_value());
+ EXPECT_TRUE(CapabilitySet::find_capability_set("vespa.container_node").has_value());
+ EXPECT_TRUE(CapabilitySet::find_capability_set("vespa.telemetry").has_value());
+ EXPECT_TRUE(CapabilitySet::find_capability_set("vespa.cluster_controller_node").has_value());
+ EXPECT_TRUE(CapabilitySet::find_capability_set("vespa.config_server").has_value());
+}
+
+TEST("Unknown capability set name returns nullopt") {
+ EXPECT_FALSE(CapabilitySet::find_capability_set("vespa.unicorn_launcher").has_value());
+}
+
+TEST("Resolving a capability set adds all its underlying capabilities") {
+ CapabilitySet caps;
+ EXPECT_TRUE(caps.resolve_and_add("vespa.content_node"));
+ // Slightly suboptimal; this test will fail if the default set of capabilities for vespa.content_node changes.
+ EXPECT_EQUAL(caps.count(), 3u);
+ EXPECT_FALSE(caps.empty());
+ EXPECT_TRUE(caps.contains(Capability::content_storage_api()));
+ EXPECT_TRUE(caps.contains(Capability::content_document_api()));
+ EXPECT_TRUE(caps.contains(Capability::slobrok_api()));
+ EXPECT_FALSE(caps.contains(Capability::content_search_api()));
+}
+
+TEST("Resolving a single capability adds it to the underlying capabilities") {
+ CapabilitySet caps;
+ EXPECT_TRUE(caps.resolve_and_add("vespa.slobrok.api"));
+ EXPECT_EQUAL(caps.count(), 1u);
+ EXPECT_FALSE(caps.empty());
+ EXPECT_TRUE(caps.contains(Capability::slobrok_api()));
+ EXPECT_FALSE(caps.contains(Capability::content_storage_api()));
+}
+
+TEST("Resolving an unknown capability set returns false and does not add anything") {
+ CapabilitySet caps;
+ EXPECT_FALSE(caps.resolve_and_add("vespa.distributors_evil_twin_with_an_evil_goatee"));
+ EXPECT_EQUAL(caps.count(), 0u);
+ EXPECT_TRUE(caps.empty());
+}
+
+TEST("Default-constructed CapabilitySet has no capabilities") {
+ CapabilitySet caps;
+ EXPECT_EQUAL(caps.count(), 0u);
+ EXPECT_TRUE(caps.empty());
+ EXPECT_FALSE(caps.contains(Capability::content_storage_api()));
+}
+
+TEST("CapabilitySet can be created with all capabilities") {
+ auto caps = CapabilitySet::make_with_all_capabilities();
+ EXPECT_EQUAL(caps.count(), max_capability_bit_count());
+ EXPECT_TRUE(caps.contains(Capability::content_storage_api()));
+ EXPECT_TRUE(caps.contains(Capability::content_metrics_api()));
+ // ... we just assume the rest are present as well.
+}
+
+TEST("CapabilitySet::contains_all() requires an intersection of capabilities") {
+ auto cap1 = Capability::content_document_api();
+ auto cap2 = Capability::content_search_api();
+ auto cap3 = Capability::content_storage_api();
+
+ const auto all_caps = CapabilitySet::make_with_all_capabilities();
+ auto set_123 = CapabilitySet::of({cap1, cap2, cap3});
+ auto set_13 = CapabilitySet::of({cap1, cap3});
+ auto set_2 = CapabilitySet::of({cap2});
+ auto set_23 = CapabilitySet::of({cap2, cap3});
+ auto empty = CapabilitySet::make_empty();
+
+ // Sets contain themselves
+ EXPECT_TRUE(all_caps.contains_all(all_caps));
+ EXPECT_TRUE(set_13.contains_all(set_13));
+ EXPECT_TRUE(set_2.contains_all(set_2));
+ EXPECT_TRUE(empty.contains_all(empty));
+
+ // Supersets contain subsets
+ EXPECT_TRUE(all_caps.contains_all(set_123));
+ EXPECT_TRUE(all_caps.contains_all(set_13));
+ EXPECT_TRUE(set_123.contains_all(set_13));
+ EXPECT_TRUE(set_2.contains_all(empty));
+
+ // Subsets do not contain supersets
+ EXPECT_FALSE(set_123.contains_all(all_caps));
+ EXPECT_FALSE(set_13.contains_all(set_123));
+ EXPECT_FALSE(empty.contains_all(set_2));
+
+ // Partially overlapping sets are not contained in each other
+ EXPECT_FALSE(set_13.contains_all(set_23));
+ EXPECT_FALSE(set_23.contains_all(set_13));
+
+ // Fully disjoint sets are not contained in each other
+ EXPECT_FALSE(set_2.contains_all(set_13));
+ EXPECT_FALSE(set_13.contains_all(set_2));
+}
+
+TEST_MAIN() { TEST_RUN_ALL(); }
diff --git a/vespalib/src/tests/net/tls/openssl_impl/openssl_impl_test.cpp b/vespalib/src/tests/net/tls/openssl_impl/openssl_impl_test.cpp
index 1de10939bea..3d19c335c19 100644
--- a/vespalib/src/tests/net/tls/openssl_impl/openssl_impl_test.cpp
+++ b/vespalib/src/tests/net/tls/openssl_impl/openssl_impl_test.cpp
@@ -542,7 +542,7 @@ struct PrintingCertificateCallback : CertificateVerificationCallback {
for (auto& dns : peer_creds.dns_sans) {
fprintf(stderr, "Got a DNS SAN entry: %s\n", dns.c_str());
}
- return VerificationResult::make_authorized_for_all_roles();
+ return VerificationResult::make_authorized_with_all_capabilities();
}
};
@@ -551,7 +551,7 @@ struct MockCertificateCallback : CertificateVerificationCallback {
mutable PeerCredentials creds; // only used in single thread testing context
VerificationResult verify(const PeerCredentials& peer_creds) const override {
creds = peer_creds;
- return VerificationResult::make_authorized_for_all_roles();
+ return VerificationResult::make_authorized_with_all_capabilities();
}
};
@@ -712,6 +712,29 @@ TEST_F("Server allows client with certificate that DOES match peer policy", Cert
EXPECT_TRUE(f.handshake());
}
+TEST_F("Authz policy-derived peer capabilities are propagated to CryptoCodec", CertFixture) {
+ auto server_ck = f.create_ca_issued_peer_cert({}, {{"DNS:hello.world.example.com"}});
+ auto authorized = authorized_peers({policy_with({required_san_dns("stale.memes.example.com")},
+ CapabilitySet::of({Capability::content_search_api(),
+ Capability::content_status_pages()})),
+ policy_with({required_san_dns("fresh.memes.example.com")},
+ CapabilitySet::make_with_all_capabilities())});
+ f.reset_server_with_cert_opts(server_ck, std::move(authorized));
+ auto client_ck = f.create_ca_issued_peer_cert({}, {{"DNS:stale.memes.example.com"}});
+ f.reset_client_with_cert_opts(client_ck, AuthorizedPeers::allow_all_authenticated());
+
+ ASSERT_TRUE(f.handshake());
+
+ // Note: "inversion" of client <-> server is because the capabilities are that of the _peer_.
+ auto client_caps = f.server->granted_capabilities();
+ auto server_caps = f.client->granted_capabilities();
+ // Server (from client's PoV) implicitly has all capabilities since client doesn't specify any policies
+ EXPECT_EQUAL(server_caps, CapabilitySet::make_with_all_capabilities());
+ // Client (from server's PoV) only has capabilities for the rule matching its DNS SAN entry
+ EXPECT_EQUAL(client_caps, CapabilitySet::of({Capability::content_search_api(),
+ Capability::content_status_pages()}));
+}
+
void reset_peers_with_server_authz_mode(CertFixture& f, AuthorizationMode authz_mode) {
auto ck = f.create_ca_issued_peer_cert({"hello.world.example.com"}, {});
diff --git a/vespalib/src/tests/net/tls/policy_checking_certificate_verifier/policy_checking_certificate_verifier_test.cpp b/vespalib/src/tests/net/tls/policy_checking_certificate_verifier/policy_checking_certificate_verifier_test.cpp
index fa2bc1a2eaf..c456d7e2a5c 100644
--- a/vespalib/src/tests/net/tls/policy_checking_certificate_verifier/policy_checking_certificate_verifier_test.cpp
+++ b/vespalib/src/tests/net/tls/policy_checking_certificate_verifier/policy_checking_certificate_verifier_test.cpp
@@ -127,9 +127,9 @@ bool verify(AuthorizedPeers authorized_peers, const PeerCredentials& peer_creds)
return verifier->verify(peer_creds).success();
}
-AssumedRoles verify_roles(AuthorizedPeers authorized_peers, const PeerCredentials& peer_creds) {
+CapabilitySet verify_capabilities(AuthorizedPeers authorized_peers, const PeerCredentials& peer_creds) {
auto verifier = create_verify_callback_from(std::move(authorized_peers));
- return verifier->verify(peer_creds).steal_assumed_roles();
+ return verifier->verify(peer_creds).granted_capabilities();
}
TEST("Default-constructed AuthorizedPeers does not allow all authenticated peers") {
@@ -142,14 +142,16 @@ TEST("Specially constructed set of policies allows all authenticated peers") {
EXPECT_TRUE(verify(allow_all, creds_with_dns_sans({{"anything.goes"}})));
}
-TEST("specially constructed set of policies returns wildcard role set") {
+TEST("specially constructed set of policies returns full capability set") {
auto allow_all = AuthorizedPeers::allow_all_authenticated();
- EXPECT_EQUAL(verify_roles(allow_all, creds_with_dns_sans({{"anything.goes"}})), AssumedRoles::make_wildcard_role());
+ EXPECT_EQUAL(verify_capabilities(allow_all, creds_with_dns_sans({{"anything.goes"}})),
+ CapabilitySet::make_with_all_capabilities());
}
-TEST("policy without explicit role set implicitly returns wildcard role set") {
+TEST("policy without explicit capability set implicitly returns full capability set") {
auto authorized = authorized_peers({policy_with({required_san_dns("yolo.swag")})});
- EXPECT_EQUAL(verify_roles(authorized, creds_with_dns_sans({{"yolo.swag"}})), AssumedRoles::make_wildcard_role());
+ EXPECT_EQUAL(verify_capabilities(authorized, creds_with_dns_sans({{"yolo.swag"}})),
+ CapabilitySet::make_with_all_capabilities());
}
TEST("Non-empty policies do not allow all authenticated peers") {
@@ -246,11 +248,11 @@ struct MultiPolicyMatchFixture {
};
MultiPolicyMatchFixture::MultiPolicyMatchFixture()
- : authorized(authorized_peers({policy_with({required_san_dns("hello.world")}, assumed_roles({"r1"})),
- policy_with({required_san_dns("foo.bar")}, assumed_roles({"r2"})),
- policy_with({required_san_dns("zoid.berg")}, assumed_roles({"r2", "r3"})),
- policy_with({required_san_dns("secret.sauce")}, AssumedRoles::make_wildcard_role()),
- policy_with({required_san_uri("zoid://be.rg/")}, assumed_roles({"r4"}))}))
+ : authorized(authorized_peers({policy_with({required_san_dns("hello.world")}, CapabilitySet::of({cap_1()})),
+ policy_with({required_san_dns("foo.bar")}, CapabilitySet::of({cap_2()})),
+ policy_with({required_san_dns("zoid.berg")}, CapabilitySet::of({cap_2(), cap_3()})),
+ policy_with({required_san_dns("secret.sauce")}, CapabilitySet::make_with_all_capabilities()),
+ policy_with({required_san_uri("zoid://be.rg/")}, CapabilitySet::of({cap_4()}))}))
{}
MultiPolicyMatchFixture::~MultiPolicyMatchFixture() = default;
@@ -262,32 +264,37 @@ TEST_F("peer verifies if it matches at least 1 policy of multiple", MultiPolicyM
EXPECT_TRUE(verify(f.authorized, creds_with_uri_sans({{"zoid://be.rg/"}})));
}
-TEST_F("role set is returned for single matched policy", MultiPolicyMatchFixture) {
- EXPECT_EQUAL(verify_roles(f.authorized, creds_with_dns_sans({{"hello.world"}})), assumed_roles({"r1"}));
- EXPECT_EQUAL(verify_roles(f.authorized, creds_with_dns_sans({{"foo.bar"}})), assumed_roles({"r2"}));
- EXPECT_EQUAL(verify_roles(f.authorized, creds_with_dns_sans({{"zoid.berg"}})), assumed_roles({"r2", "r3"}));
- EXPECT_EQUAL(verify_roles(f.authorized, creds_with_dns_sans({{"secret.sauce"}})), AssumedRoles::make_wildcard_role());
- EXPECT_EQUAL(verify_roles(f.authorized, creds_with_uri_sans({{"zoid://be.rg/"}})), assumed_roles({"r4"}));
+TEST_F("capability set is returned for single matched policy", MultiPolicyMatchFixture) {
+ EXPECT_EQUAL(verify_capabilities(f.authorized, creds_with_dns_sans({{"hello.world"}})),
+ CapabilitySet::of({cap_1()}));
+ EXPECT_EQUAL(verify_capabilities(f.authorized, creds_with_dns_sans({{"foo.bar"}})),
+ CapabilitySet::of({cap_2()}));
+ EXPECT_EQUAL(verify_capabilities(f.authorized, creds_with_dns_sans({{"zoid.berg"}})),
+ CapabilitySet::of({cap_2(), cap_3()}));
+ EXPECT_EQUAL(verify_capabilities(f.authorized, creds_with_dns_sans({{"secret.sauce"}})),
+ CapabilitySet::make_with_all_capabilities());
+ EXPECT_EQUAL(verify_capabilities(f.authorized, creds_with_uri_sans({{"zoid://be.rg/"}})),
+ CapabilitySet::of({cap_4()}));
}
TEST_F("peer verifies if it matches multiple policies", MultiPolicyMatchFixture) {
EXPECT_TRUE(verify(f.authorized, creds_with_dns_sans({{"hello.world"}, {"zoid.berg"}})));
}
-TEST_F("union role set is returned if multiple policies match", MultiPolicyMatchFixture) {
- EXPECT_EQUAL(verify_roles(f.authorized, creds_with_dns_sans({{"hello.world"}, {"foo.bar"}, {"zoid.berg"}})),
- assumed_roles({"r1", "r2", "r3"}));
- // Wildcard role is tracked as a distinct role string
- EXPECT_EQUAL(verify_roles(f.authorized, creds_with_dns_sans({{"hello.world"}, {"foo.bar"}, {"secret.sauce"}})),
- assumed_roles({"r1", "r2", "*"}));
+TEST_F("union capability set is returned if multiple policies match", MultiPolicyMatchFixture) {
+ EXPECT_EQUAL(verify_capabilities(f.authorized, creds_with_dns_sans({{"hello.world"}, {"foo.bar"}, {"zoid.berg"}})),
+ CapabilitySet::of({cap_1(), cap_2(), cap_3()}));
+ EXPECT_EQUAL(verify_capabilities(f.authorized, creds_with_dns_sans({{"hello.world"}, {"foo.bar"}, {"secret.sauce"}})),
+ CapabilitySet::make_with_all_capabilities());
}
TEST_F("peer must match at least 1 of multiple policies", MultiPolicyMatchFixture) {
EXPECT_FALSE(verify(f.authorized, creds_with_dns_sans({{"does.not.exist"}})));
}
-TEST_F("empty role set is returned if no policies match", MultiPolicyMatchFixture) {
- EXPECT_EQUAL(verify_roles(f.authorized, creds_with_dns_sans({{"does.not.exist"}})), AssumedRoles::make_empty());
+TEST_F("empty capability set is returned if no policies match", MultiPolicyMatchFixture) {
+ EXPECT_EQUAL(verify_capabilities(f.authorized, creds_with_dns_sans({{"does.not.exist"}})),
+ CapabilitySet::make_empty());
}
TEST("CN requirement without glob pattern is matched as exact string") {
@@ -308,62 +315,32 @@ TEST("CN requirement can include glob wildcards") {
EXPECT_FALSE(verify(authorized, creds_with_cn("world")));
}
-TEST("AssumedRoles by default contains no roles") {
- AssumedRoles roles;
- EXPECT_TRUE(roles.empty());
- EXPECT_FALSE(roles.can_assume_role("foo"));
- auto empty = AssumedRoles::make_empty();
- EXPECT_EQUAL(roles, empty);
-}
-
-TEST("AssumedRoles can be constructed with an explicit set of roles") {
- auto roles = AssumedRoles::make_for_roles({"foo", "bar"});
- EXPECT_TRUE(roles.can_assume_role("foo"));
- EXPECT_TRUE(roles.can_assume_role("bar"));
- EXPECT_FALSE(roles.can_assume_role("baz"));
-}
-
-TEST("AssumedRoles wildcard role can assume any role") {
- auto roles = AssumedRoles::make_wildcard_role();
- EXPECT_TRUE(roles.can_assume_role("foo"));
- EXPECT_TRUE(roles.can_assume_role("bar"));
-}
-
-TEST("AssumedRolesBuilder builds union set of added roles") {
- AssumedRolesBuilder builder;
- builder.add_union(AssumedRoles::make_for_roles({"hello", "world"}));
- builder.add_union(AssumedRoles::make_for_roles({"hello", "moon"}));
- builder.add_union(AssumedRoles::make_for_roles({"goodbye", "moon"}));
- auto roles = builder.build_with_move();
- EXPECT_EQUAL(roles, AssumedRoles::make_for_roles({"hello", "goodbye", "moon", "world"}));
-}
-
TEST("VerificationResult is not authorized by default") {
VerificationResult result;
EXPECT_FALSE(result.success());
- EXPECT_TRUE(result.assumed_roles().empty());
+ EXPECT_TRUE(result.granted_capabilities().empty());
}
TEST("VerificationResult can be explicitly created as not authorized") {
auto result = VerificationResult::make_not_authorized();
EXPECT_FALSE(result.success());
- EXPECT_TRUE(result.assumed_roles().empty());
+ EXPECT_TRUE(result.granted_capabilities().empty());
}
-TEST("VerificationResult can be pre-authorized for all roles") {
- auto result = VerificationResult::make_authorized_for_all_roles();
+TEST("VerificationResult can be pre-authorized with all capabilities") {
+ auto result = VerificationResult::make_authorized_with_all_capabilities();
EXPECT_TRUE(result.success());
- EXPECT_FALSE(result.assumed_roles().empty());
- EXPECT_TRUE(result.assumed_roles().can_assume_role("foo"));
+ EXPECT_FALSE(result.granted_capabilities().empty());
+ EXPECT_EQUAL(result.granted_capabilities(), CapabilitySet::make_with_all_capabilities());
}
-TEST("VerificationResult can be pre-authorized for an explicit set of roles") {
- auto result = VerificationResult::make_authorized_for_roles(AssumedRoles::make_for_roles({"elden", "ring"}));
+TEST("VerificationResult can be pre-authorized for an explicit set of capabilities") {
+ auto result = VerificationResult::make_authorized_with_capabilities(CapabilitySet::of({cap_2(), cap_3()}));
EXPECT_TRUE(result.success());
- EXPECT_FALSE(result.assumed_roles().empty());
- EXPECT_TRUE(result.assumed_roles().can_assume_role("elden"));
- EXPECT_TRUE(result.assumed_roles().can_assume_role("ring"));
- EXPECT_FALSE(result.assumed_roles().can_assume_role("O you don't have the right"));
+ EXPECT_FALSE(result.granted_capabilities().empty());
+ EXPECT_TRUE(result.granted_capabilities().contains(cap_2()));
+ EXPECT_TRUE(result.granted_capabilities().contains(cap_3()));
+ EXPECT_FALSE(result.granted_capabilities().contains(cap_1()));
}
// TODO test CN _and_ SAN
diff --git a/vespalib/src/tests/net/tls/transport_options/transport_options_reading_test.cpp b/vespalib/src/tests/net/tls/transport_options/transport_options_reading_test.cpp
index be2c63b03f2..8d49bdbf73d 100644
--- a/vespalib/src/tests/net/tls/transport_options/transport_options_reading_test.cpp
+++ b/vespalib/src/tests/net/tls/transport_options/transport_options_reading_test.cpp
@@ -198,6 +198,68 @@ TEST("unknown fields are ignored at parse-time") {
EXPECT_TRUE(read_options_from_json_string(json).get() != nullptr); // And no exception thrown.
}
+TEST("policy without explicit capabilities implicitly get all capabilities") {
+ const char* json = R"({
+ "required-credentials":[
+ {"field": "SAN_DNS", "must-match": "hello.world"}
+ ]
+ })";
+ EXPECT_EQUAL(authorized_peers({policy_with({required_san_dns("hello.world")},
+ CapabilitySet::make_with_all_capabilities())}),
+ parse_policies(json).authorized_peers());
+}
+
+TEST("specifying a capability set adds all its underlying capabilities") {
+ const char* json = R"({
+ "required-credentials":[
+ {"field": "SAN_DNS", "must-match": "*.cool-content-clusters.example" }
+ ],
+ "capabilities": ["vespa.content_node"]
+ })";
+ EXPECT_EQUAL(authorized_peers({policy_with({required_san_dns("*.cool-content-clusters.example")},
+ CapabilitySet::content_node())}),
+ parse_policies(json).authorized_peers());
+}
+
+TEST("can specify single leaf capabilities") {
+ const char* json = R"({
+ "required-credentials":[
+ {"field": "SAN_DNS", "must-match": "*.cool-content-clusters.example" }
+ ],
+ "capabilities": ["vespa.content.metrics_api", "vespa.slobrok.api"]
+ })";
+ EXPECT_EQUAL(authorized_peers({policy_with({required_san_dns("*.cool-content-clusters.example")},
+ CapabilitySet::of({Capability::content_metrics_api(),
+ Capability::slobrok_api()}))}),
+ parse_policies(json).authorized_peers());
+}
+
+TEST("specifying multiple capability sets adds union of underlying capabilities") {
+ const char* json = R"({
+ "required-credentials":[
+ {"field": "SAN_DNS", "must-match": "*.cool-content-clusters.example" }
+ ],
+ "capabilities": ["vespa.content_node", "vespa.container_node"]
+ })";
+ CapabilitySet caps;
+ caps.add_all(CapabilitySet::content_node());
+ caps.add_all(CapabilitySet::container_node());
+ EXPECT_EQUAL(authorized_peers({policy_with({required_san_dns("*.cool-content-clusters.example")}, caps)}),
+ parse_policies(json).authorized_peers());
+}
+
+TEST("empty capabilities array is not allowed") {
+ const char* json = R"({
+ "required-credentials":[
+ {"field": "SAN_DNS", "must-match": "*.cool-content-clusters.example" }
+ ],
+ "capabilities": []
+ })";
+ EXPECT_EXCEPTION(parse_policies(json), vespalib::IllegalArgumentException,
+ "\"capabilities\" array must either be not present (implies "
+ "all capabilities) or contain at least one capability name");
+}
+
// TODO test parsing of multiple policies
TEST_MAIN() { TEST_RUN_ALL(); }
diff --git a/vespalib/src/vespa/vespalib/net/CMakeLists.txt b/vespalib/src/vespa/vespalib/net/CMakeLists.txt
index e3eb32d3775..05c404ec2a7 100644
--- a/vespalib/src/vespa/vespalib/net/CMakeLists.txt
+++ b/vespalib/src/vespa/vespalib/net/CMakeLists.txt
@@ -9,6 +9,7 @@ endif()
vespa_add_library(vespalib_vespalib_net OBJECT
SOURCES
async_resolver.cpp
+ connection_auth_context.cpp
crypto_engine.cpp
crypto_socket.cpp
selector.cpp
diff --git a/vespalib/src/vespa/vespalib/net/connection_auth_context.cpp b/vespalib/src/vespa/vespalib/net/connection_auth_context.cpp
new file mode 100644
index 00000000000..5dd41b3b4d5
--- /dev/null
+++ b/vespalib/src/vespa/vespalib/net/connection_auth_context.cpp
@@ -0,0 +1,21 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+#include "connection_auth_context.h"
+
+namespace vespalib::net {
+
+ConnectionAuthContext::ConnectionAuthContext(tls::PeerCredentials peer_credentials,
+ tls::CapabilitySet capabilities) noexcept
+ : _peer_credentials(std::move(peer_credentials)),
+ _capabilities(std::move(capabilities))
+{
+}
+
+ConnectionAuthContext::ConnectionAuthContext(const ConnectionAuthContext&) = default;
+ConnectionAuthContext& ConnectionAuthContext::operator=(const ConnectionAuthContext&) = default;
+ConnectionAuthContext::ConnectionAuthContext(ConnectionAuthContext&&) noexcept = default;
+ConnectionAuthContext& ConnectionAuthContext::operator=(ConnectionAuthContext&&) noexcept = default;
+
+ConnectionAuthContext::~ConnectionAuthContext() = default;
+
+}
diff --git a/vespalib/src/vespa/vespalib/net/connection_auth_context.h b/vespalib/src/vespa/vespalib/net/connection_auth_context.h
new file mode 100644
index 00000000000..fc9815f8b8e
--- /dev/null
+++ b/vespalib/src/vespa/vespalib/net/connection_auth_context.h
@@ -0,0 +1,29 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+#pragma once
+
+// TODO consider moving out of tls sub-namespace
+#include <vespa/vespalib/net/tls/peer_credentials.h>
+#include <vespa/vespalib/net/tls/capability_set.h>
+
+namespace vespalib::net {
+
+class ConnectionAuthContext {
+ tls::PeerCredentials _peer_credentials;
+ tls::CapabilitySet _capabilities;
+public:
+ ConnectionAuthContext(tls::PeerCredentials peer_credentials,
+ tls::CapabilitySet capabilities) noexcept;
+
+ ConnectionAuthContext(const ConnectionAuthContext&);
+ ConnectionAuthContext& operator=(const ConnectionAuthContext&);
+ ConnectionAuthContext(ConnectionAuthContext&&) noexcept;
+ ConnectionAuthContext& operator=(ConnectionAuthContext&&) noexcept;
+
+ ~ConnectionAuthContext();
+
+ const tls::PeerCredentials& peer_credentials() const noexcept { return _peer_credentials; }
+ const tls::CapabilitySet& capabilities() const noexcept { return _capabilities; }
+};
+
+}
diff --git a/vespalib/src/vespa/vespalib/net/crypto_socket.cpp b/vespalib/src/vespa/vespalib/net/crypto_socket.cpp
index 8d3116339a3..0ae90be8539 100644
--- a/vespalib/src/vespa/vespalib/net/crypto_socket.cpp
+++ b/vespalib/src/vespa/vespalib/net/crypto_socket.cpp
@@ -1,9 +1,18 @@
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
#include "crypto_socket.h"
+#include <vespa/vespalib/net/connection_auth_context.h>
namespace vespalib {
CryptoSocket::~CryptoSocket() = default;
+std::unique_ptr<net::ConnectionAuthContext>
+CryptoSocket::make_auth_context()
+{
+ return std::make_unique<net::ConnectionAuthContext>(
+ net::tls::PeerCredentials(),
+ net::tls::CapabilitySet::make_with_all_capabilities());
+}
+
} // namespace vespalib
diff --git a/vespalib/src/vespa/vespalib/net/crypto_socket.h b/vespalib/src/vespa/vespalib/net/crypto_socket.h
index a6ed6553bbe..9ae2af08084 100644
--- a/vespalib/src/vespa/vespalib/net/crypto_socket.h
+++ b/vespalib/src/vespa/vespalib/net/crypto_socket.h
@@ -7,6 +7,8 @@
namespace vespalib {
+namespace net { class ConnectionAuthContext; }
+
/**
* Abstraction of a low-level async network socket which can produce
* io events and allows encrypting written data and decrypting read
@@ -143,6 +145,18 @@ struct CryptoSocket {
**/
virtual void drop_empty_buffers() = 0;
+ /**
+ * If the underlying transport channel supports authn/authz,
+ * returns a new ConnectionAuthContext object containing the verified
+ * credentials of the peer as well as the resulting peer capabilities
+ * inferred by our own policy matching.
+ *
+ * If the underlying transport channel does _not_ support authn/authz
+ * (such as a plaintext connection) a dummy context is returned which
+ * offers _all_ capabilities.
+ */
+ [[nodiscard]] virtual std::unique_ptr<net::ConnectionAuthContext> make_auth_context();
+
virtual ~CryptoSocket();
};
diff --git a/vespalib/src/vespa/vespalib/net/tls/CMakeLists.txt b/vespalib/src/vespa/vespalib/net/tls/CMakeLists.txt
index a94d088b6a8..5be2e0d4387 100644
--- a/vespalib/src/vespa/vespalib/net/tls/CMakeLists.txt
+++ b/vespalib/src/vespa/vespalib/net/tls/CMakeLists.txt
@@ -1,9 +1,10 @@
# Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
vespa_add_library(vespalib_vespalib_net_tls OBJECT
SOURCES
- assumed_roles.cpp
authorization_mode.cpp
auto_reloading_tls_crypto_engine.cpp
+ capability.cpp
+ capability_set.cpp
crypto_codec.cpp
crypto_codec_adapter.cpp
maybe_tls_crypto_engine.cpp
diff --git a/vespalib/src/vespa/vespalib/net/tls/assumed_roles.cpp b/vespalib/src/vespa/vespalib/net/tls/assumed_roles.cpp
deleted file mode 100644
index 672458d0024..00000000000
--- a/vespalib/src/vespa/vespalib/net/tls/assumed_roles.cpp
+++ /dev/null
@@ -1,95 +0,0 @@
-// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-#include "assumed_roles.h"
-#include <vespa/vespalib/stllike/asciistream.h>
-#include <algorithm>
-#include <ostream>
-
-namespace vespalib::net::tls {
-
-const string AssumedRoles::WildcardRole("*");
-
-AssumedRoles::AssumedRoles() = default;
-
-AssumedRoles::AssumedRoles(RoleSet assumed_roles)
- : _assumed_roles(std::move(assumed_roles))
-{}
-
-AssumedRoles::AssumedRoles(const AssumedRoles&) = default;
-AssumedRoles& AssumedRoles::operator=(const AssumedRoles&) = default;
-AssumedRoles::AssumedRoles(AssumedRoles&&) noexcept = default;
-AssumedRoles& AssumedRoles::operator=(AssumedRoles&&) noexcept = default;
-AssumedRoles::~AssumedRoles() = default;
-
-bool AssumedRoles::can_assume_role(const string& role) const noexcept {
- return (_assumed_roles.contains(role) || _assumed_roles.contains(WildcardRole));
-}
-
-std::vector<string> AssumedRoles::ordered_roles() const {
- std::vector<string> roles;
- for (const auto& r : _assumed_roles) {
- roles.emplace_back(r);
- }
- std::sort(roles.begin(), roles.end());
- return roles;
-}
-
-bool AssumedRoles::operator==(const AssumedRoles& rhs) const noexcept {
- return (_assumed_roles == rhs._assumed_roles);
-}
-
-void AssumedRoles::print(asciistream& os) const {
- os << "AssumedRoles(roles: [";
- auto roles = ordered_roles();
- for (size_t i = 0; i < roles.size(); ++i) {
- if (i > 0) {
- os << ", ";
- }
- os << roles[i];
- }
- os << "])";
-}
-
-asciistream& operator<<(asciistream& os, const AssumedRoles& res) {
- res.print(os);
- return os;
-}
-
-std::ostream& operator<<(std::ostream& os, const AssumedRoles& res) {
- os << to_string(res);
- return os;
-}
-
-string to_string(const AssumedRoles& res) {
- asciistream os;
- os << res;
- return os.str();
-}
-
-AssumedRoles AssumedRoles::make_for_roles(RoleSet assumed_roles) {
- return AssumedRoles(std::move(assumed_roles));
-}
-
-AssumedRoles AssumedRoles::make_wildcard_role() {
- return AssumedRoles(RoleSet({WildcardRole}));
-}
-
-AssumedRoles AssumedRoles::make_empty() {
- return {};
-}
-
-AssumedRolesBuilder::AssumedRolesBuilder() = default;
-AssumedRolesBuilder::~AssumedRolesBuilder() = default;
-
-void AssumedRolesBuilder::add_union(const AssumedRoles& roles) {
- // TODO fix hash_set iterator range insert()
- for (const auto& role : roles.unordered_roles()) {
- _wip_roles.insert(role);
- }
-}
-
-AssumedRoles AssumedRolesBuilder::build_with_move() {
- return AssumedRoles::make_for_roles(std::move(_wip_roles));
-}
-
-}
-
diff --git a/vespalib/src/vespa/vespalib/net/tls/assumed_roles.h b/vespalib/src/vespa/vespalib/net/tls/assumed_roles.h
deleted file mode 100644
index 00d800916fd..00000000000
--- a/vespalib/src/vespa/vespalib/net/tls/assumed_roles.h
+++ /dev/null
@@ -1,80 +0,0 @@
-// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-#pragma once
-
-#include <vespa/vespalib/stllike/hash_set.h>
-#include <vespa/vespalib/stllike/string.h>
-#include <vector>
-#include <iosfwd>
-
-namespace vespalib { class asciistream; }
-
-namespace vespalib::net::tls {
-
-/**
- * Encapsulates a set of roles that requests over a particular authenticated
- * connection can assume, based on the authorization rules it matched during mTLS
- * handshaking.
- *
- * If at least one role is a wildcard ('*') role, the connection can assume _any_
- * possible role. This is the default when no role constraints are specified in
- * the TLS configuration file (legacy behavior). However, a default-constructed
- * AssumedRoles instance does not allow any roles to be assumed.
- */
-class AssumedRoles {
-public:
- using RoleSet = hash_set<string>;
-private:
- RoleSet _assumed_roles;
-
- static const string WildcardRole;
-
- explicit AssumedRoles(RoleSet assumed_roles);
-public:
- AssumedRoles();
- AssumedRoles(const AssumedRoles&);
- AssumedRoles& operator=(const AssumedRoles&);
- AssumedRoles(AssumedRoles&&) noexcept;
- AssumedRoles& operator=(AssumedRoles&&) noexcept;
- ~AssumedRoles();
-
- [[nodiscard]] bool empty() const noexcept {
- return _assumed_roles.empty();
- }
-
- /**
- * Returns true iff `role` is present in the role set OR the role set contains
- * the special wildcard role.
- */
- [[nodiscard]] bool can_assume_role(const string& role) const noexcept;
-
- [[nodiscard]] const RoleSet& unordered_roles() const noexcept {
- return _assumed_roles;
- }
-
- [[nodiscard]] std::vector<string> ordered_roles() const;
-
- bool operator==(const AssumedRoles& rhs) const noexcept;
-
- void print(asciistream& os) const;
-
- static AssumedRoles make_for_roles(RoleSet assumed_roles);
- static AssumedRoles make_wildcard_role(); // Allows assuming _all_ possible roles
- static AssumedRoles make_empty(); // Matches _no_ possible roles
-};
-
-asciistream& operator<<(asciistream&, const AssumedRoles&);
-std::ostream& operator<<(std::ostream&, const AssumedRoles&);
-string to_string(const AssumedRoles&);
-
-class AssumedRolesBuilder {
- AssumedRoles::RoleSet _wip_roles;
-public:
- AssumedRolesBuilder();
- ~AssumedRolesBuilder();
-
- void add_union(const AssumedRoles& roles);
- [[nodiscard]] bool empty() const noexcept { return _wip_roles.empty(); }
- [[nodiscard]] AssumedRoles build_with_move();
-};
-
-}
diff --git a/vespalib/src/vespa/vespalib/net/tls/capability.cpp b/vespalib/src/vespa/vespalib/net/tls/capability.cpp
new file mode 100644
index 00000000000..e114ea174b9
--- /dev/null
+++ b/vespalib/src/vespa/vespalib/net/tls/capability.cpp
@@ -0,0 +1,62 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+#include "capability.h"
+#include <vespa/vespalib/stllike/hash_map.hpp>
+#include <vespa/vespalib/stllike/asciistream.h>
+#include <array>
+
+namespace vespalib::net::tls {
+
+namespace {
+
+using namespace std::string_view_literals;
+
+// Important: must match 1-1 with CapabilityId values!
+constexpr std::array<std::string_view, max_capability_bit_count()> capability_names = {
+ "vespa.content.storage_api"sv,
+ "vespa.content.document_api"sv,
+ "vespa.content.search_api"sv,
+ "vespa.content.cluster_controller.internal_state_api"sv,
+ "vespa.slobrok.api"sv,
+ "vespa.content.status_pages"sv,
+ "vespa.content.metrics_api"sv,
+};
+
+} // anon ns
+
+std::string_view Capability::name() const noexcept {
+ return capability_names[id_bit_pos()];
+}
+
+string Capability::to_string() const {
+ asciistream os;
+ // TODO asciistream should be made std::string_view-aware
+ os << "Capability(" << stringref(name().data(), name().length()) << ')';
+ return os.str();
+}
+
+std::optional<Capability> Capability::find_capability(const string& cap_name) noexcept {
+ static const hash_map<string, Capability> name_to_cap({
+ {"vespa.content.storage_api", content_storage_api()},
+ {"vespa.content.document_api", content_document_api()},
+ {"vespa.content.search_api", content_search_api()},
+ {"vespa.content.cluster_controller.internal_state_api", content_cluster_controller_internal_state_api()},
+ {"vespa.slobrok.api", slobrok_api()},
+ {"vespa.content.status_pages", content_status_pages()},
+ {"vespa.content.metrics_api", content_metrics_api()},
+ });
+ auto iter = name_to_cap.find(cap_name);
+ return (iter != name_to_cap.end()) ? std::optional<Capability>(iter->second) : std::nullopt;
+}
+
+std::ostream& operator<<(std::ostream& os, const Capability& cap) {
+ os << cap.to_string();
+ return os;
+}
+
+asciistream& operator<<(asciistream& os, const Capability& cap) {
+ os << cap.to_string();
+ return os;
+}
+
+}
diff --git a/vespalib/src/vespa/vespalib/net/tls/capability.h b/vespalib/src/vespa/vespalib/net/tls/capability.h
new file mode 100644
index 00000000000..67d8067977b
--- /dev/null
+++ b/vespalib/src/vespa/vespalib/net/tls/capability.h
@@ -0,0 +1,109 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+#pragma once
+
+#include <vespa/vespalib/stllike/string.h>
+#include <bitset>
+#include <iosfwd>
+#include <optional>
+#include <string_view>
+#include <vector>
+
+namespace vespalib { class asciistream; }
+
+namespace vespalib::net::tls {
+
+// Each ID value corresponds to a unique single-bit position.
+// These values shall never be exposed outside the running process, i.e. they
+// must be possible to change arbitrarily internally across versions.
+enum class CapabilityId : uint32_t {
+ ContentStorageApi = 0, // Must start at zero
+ ContentDocumentApi,
+ ContentSearchApi,
+ ContentClusterControllerInternalStateApi,
+ SlobrokApi,
+ ContentStatusPages,
+ ContentMetricsApi,
+ // When adding a capability ID to the end, max_capability_bit_count() MUST be updated
+};
+
+constexpr size_t max_capability_bit_count() noexcept {
+ // This must refer to the highest possible CapabilityId enum value.
+ return static_cast<size_t>(CapabilityId::ContentMetricsApi) + 1;
+}
+
+using CapabilityBitSet = std::bitset<max_capability_bit_count()>;
+
+/**
+ * A capability represents the ability to access a distinct service or API
+ * plane in Vespa (such as the Document API).
+ *
+ * Capability instances are intended to be very cheap to pass and store by value.
+ */
+class Capability {
+private:
+ CapabilityId _cap_id;
+
+ constexpr explicit Capability(CapabilityId cap_id) noexcept : _cap_id(cap_id) {}
+public:
+ Capability() = delete; // Only valid capabilities can be created.
+
+ constexpr CapabilityId id() const noexcept { return _cap_id; }
+
+ constexpr uint32_t id_bit_pos() const noexcept { return static_cast<uint32_t>(_cap_id); }
+
+ constexpr CapabilityBitSet id_as_bit_set() const noexcept {
+ static_assert(max_capability_bit_count() <= 32); // Must fit into uint32_t bitmask
+ return {uint32_t(1) << id_bit_pos()};
+ }
+
+ constexpr bool operator==(const Capability& rhs) const noexcept {
+ return (_cap_id == rhs._cap_id);
+ }
+
+ constexpr bool operator!=(const Capability& rhs) const noexcept {
+ return !(*this == rhs);
+ }
+
+ std::string_view name() const noexcept;
+ string to_string() const;
+
+ constexpr static Capability of(CapabilityId id) noexcept {
+ return Capability(id);
+ }
+
+ static std::optional<Capability> find_capability(const string& cap_name) noexcept;
+
+ constexpr static Capability content_storage_api() noexcept {
+ return Capability(CapabilityId::ContentStorageApi);
+ }
+
+ constexpr static Capability content_document_api() noexcept {
+ return Capability(CapabilityId::ContentDocumentApi);
+ }
+
+ constexpr static Capability content_search_api() noexcept {
+ return Capability(CapabilityId::ContentSearchApi);
+ }
+
+ constexpr static Capability content_cluster_controller_internal_state_api() noexcept {
+ return Capability(CapabilityId::ContentClusterControllerInternalStateApi);
+ }
+
+ constexpr static Capability slobrok_api() noexcept {
+ return Capability(CapabilityId::SlobrokApi);
+ }
+
+ constexpr static Capability content_status_pages() noexcept {
+ return Capability(CapabilityId::ContentStatusPages);
+ }
+
+ constexpr static Capability content_metrics_api() noexcept {
+ return Capability(CapabilityId::ContentMetricsApi);
+ }
+
+};
+
+std::ostream& operator<<(std::ostream&, const Capability& cap);
+asciistream& operator<<(asciistream&, const Capability& cap);
+
+}
diff --git a/vespalib/src/vespa/vespalib/net/tls/capability_set.cpp b/vespalib/src/vespa/vespalib/net/tls/capability_set.cpp
new file mode 100644
index 00000000000..d0d25924960
--- /dev/null
+++ b/vespalib/src/vespa/vespalib/net/tls/capability_set.cpp
@@ -0,0 +1,95 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+#include "capability_set.h"
+#include <vespa/vespalib/stllike/hash_map.hpp>
+#include <vespa/vespalib/stllike/asciistream.h>
+#include <cassert>
+
+namespace vespalib::net::tls {
+
+string CapabilitySet::to_string() const {
+ asciistream os;
+ os << "CapabilitySet({";
+ bool emit_comma = false;
+ for_each_capability([&emit_comma, &os](Capability cap) {
+ if (emit_comma) {
+ os << ", ";
+ } else {
+ emit_comma = true;
+ }
+ // TODO let asciistream and std::string_view play along
+ os << stringref(cap.name().data(), cap.name().size());
+ });
+ os << "})";
+ return os.str();
+}
+
+std::optional<CapabilitySet> CapabilitySet::find_capability_set(const string& cap_set_name) noexcept {
+ static const hash_map<string, CapabilitySet> name_to_cap_set({
+ {"vespa.content_node", content_node()},
+ {"vespa.container_node", container_node()},
+ {"vespa.telemetry", telemetry()},
+ {"vespa.cluster_controller_node", cluster_controller_node()},
+ {"vespa.config_server", config_server()}
+ });
+ auto iter = name_to_cap_set.find(cap_set_name);
+ return (iter != name_to_cap_set.end()) ? std::optional<CapabilitySet>(iter->second) : std::nullopt;
+}
+
+bool CapabilitySet::resolve_and_add(const string& set_or_cap_name) noexcept {
+ if (auto cap_set = find_capability_set(set_or_cap_name)) {
+ _capability_mask |= cap_set->_capability_mask;
+ return true;
+ } else if (auto cap = Capability::find_capability(set_or_cap_name)) {
+ _capability_mask |= cap->id_as_bit_set();
+ return true;
+ }
+ return false;
+}
+
+// Note: the capability set factory functions below are all just using constexpr and/or inline
+// functions, so the compiler will happily optimize them to just "return <constant bit pattern>".
+
+CapabilitySet CapabilitySet::content_node() noexcept {
+ return CapabilitySet::of({Capability::content_storage_api(),
+ Capability::content_document_api(),
+ Capability::slobrok_api()});
+}
+
+CapabilitySet CapabilitySet::container_node() noexcept {
+ return CapabilitySet::of({Capability::content_document_api(),
+ Capability::content_search_api(),
+ Capability::slobrok_api()});
+}
+
+CapabilitySet CapabilitySet::telemetry() noexcept {
+ return CapabilitySet::of({Capability::content_status_pages(),
+ Capability::content_metrics_api()});
+}
+
+CapabilitySet CapabilitySet::cluster_controller_node() noexcept {
+ return CapabilitySet::of({Capability::content_cluster_controller_internal_state_api(),
+ Capability::slobrok_api()});
+}
+
+CapabilitySet CapabilitySet::config_server() noexcept {
+ return CapabilitySet::of({/*TODO define required capabilities*/});
+}
+
+CapabilitySet CapabilitySet::make_with_all_capabilities() noexcept {
+ CapabilityBitSet bit_set;
+ bit_set.flip(); // All cap bits set
+ return CapabilitySet(bit_set);
+}
+
+std::ostream& operator<<(std::ostream& os, const CapabilitySet& cap_set) {
+ os << cap_set.to_string();
+ return os;
+}
+
+asciistream& operator<<(asciistream& os, const CapabilitySet& cap_set) {
+ os << cap_set.to_string();
+ return os;
+}
+
+}
diff --git a/vespalib/src/vespa/vespalib/net/tls/capability_set.h b/vespalib/src/vespa/vespalib/net/tls/capability_set.h
new file mode 100644
index 00000000000..99c78105924
--- /dev/null
+++ b/vespalib/src/vespa/vespalib/net/tls/capability_set.h
@@ -0,0 +1,104 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+#pragma once
+
+#include "capability.h"
+#include <vespa/vespalib/stllike/string.h>
+#include <vespa/vespalib/stllike/hash_set.h>
+#include <bitset>
+#include <initializer_list>
+#include <iosfwd>
+#include <optional>
+#include <vector>
+
+namespace vespalib { class asciistream; }
+
+namespace vespalib::net::tls {
+
+/**
+ * A CapabilitySet efficiently represents a finite set (possibly empty) of individual
+ * capabilities and allows for both single and set-based membership tests.
+ *
+ * Factory functions are provided for all predefined Vespa capability sets.
+ *
+ * CapabilitySet instances are intended to be very cheap to pass and store by value.
+ */
+class CapabilitySet {
+ CapabilityBitSet _capability_mask;
+
+ explicit constexpr CapabilitySet(CapabilityBitSet capabilities) noexcept
+ : _capability_mask(capabilities)
+ {}
+public:
+ constexpr CapabilitySet() noexcept = default;
+ constexpr ~CapabilitySet() = default;
+
+ string to_string() const;
+
+ bool operator==(const CapabilitySet& rhs) const noexcept {
+ return (_capability_mask == rhs._capability_mask);
+ }
+
+ [[nodiscard]] bool empty() const noexcept {
+ return _capability_mask.none();
+ }
+ size_t count() const noexcept {
+ return _capability_mask.count();
+ }
+
+ [[nodiscard]] constexpr bool contains(Capability cap) const noexcept {
+ return _capability_mask[cap.id_bit_pos()];
+ }
+ [[nodiscard]] bool contains_all(CapabilitySet caps) const noexcept {
+ return ((_capability_mask & caps._capability_mask) == caps._capability_mask);
+ }
+
+ void add(const Capability& cap) noexcept {
+ _capability_mask |= cap.id_as_bit_set();
+ }
+ void add_all(const CapabilitySet& cap_set) noexcept {
+ _capability_mask |= cap_set._capability_mask;
+ }
+
+ template <typename Func>
+ void for_each_capability(Func f) const noexcept(noexcept(f(Capability::content_storage_api()))) {
+ for (size_t i = 0; i < _capability_mask.size(); ++i) {
+ if (_capability_mask[i]) {
+ f(Capability::of(static_cast<CapabilityId>(i)));
+ }
+ }
+ }
+
+ /**
+ * Since we have two capability naming "tiers", resolving is done in two steps:
+ * 1. Check if the name matches a known capability _set_ name. If so, add
+ * all unique capabilities within the set to our own working set. Return true.
+ * 2. Check if the name matches a known single capability. If so, add that
+ * capability to our own working set. Return true.
+ * 3. Otherwise, return false.
+ */
+ [[nodiscard]] bool resolve_and_add(const string& set_or_cap_name) noexcept;
+
+ static std::optional<CapabilitySet> find_capability_set(const string& cap_set_name) noexcept;
+
+ static CapabilitySet of(std::initializer_list<Capability> caps) noexcept {
+ CapabilitySet set;
+ for (const auto& cap : caps) {
+ set._capability_mask |= cap.id_as_bit_set();
+ }
+ return set;
+ }
+
+ static CapabilitySet content_node() noexcept;
+ static CapabilitySet container_node() noexcept;
+ static CapabilitySet telemetry() noexcept;
+ static CapabilitySet cluster_controller_node() noexcept;
+ static CapabilitySet config_server() noexcept;
+
+ static CapabilitySet make_with_all_capabilities() noexcept;
+ static CapabilitySet make_empty() noexcept { return CapabilitySet(); };
+};
+
+std::ostream& operator<<(std::ostream&, const CapabilitySet& cap_set);
+asciistream& operator<<(asciistream&, const CapabilitySet& cap_set);
+
+}
diff --git a/vespalib/src/vespa/vespalib/net/tls/certificate_verification_callback.h b/vespalib/src/vespa/vespalib/net/tls/certificate_verification_callback.h
index f4d8d39206b..c670d54273e 100644
--- a/vespalib/src/vespa/vespalib/net/tls/certificate_verification_callback.h
+++ b/vespalib/src/vespa/vespalib/net/tls/certificate_verification_callback.h
@@ -22,7 +22,7 @@ struct CertificateVerificationCallback {
// and it is signed by a trusted CA.
struct AcceptAllPreVerifiedCertificates : CertificateVerificationCallback {
VerificationResult verify([[maybe_unused]] const PeerCredentials& peer_creds) const override {
- return VerificationResult::make_authorized_for_all_roles(); // yolo
+ return VerificationResult::make_authorized_with_all_capabilities(); // yolo
}
};
diff --git a/vespalib/src/vespa/vespalib/net/tls/crypto_codec.h b/vespalib/src/vespa/vespalib/net/tls/crypto_codec.h
index fc729d7dd6a..8b2f258199b 100644
--- a/vespalib/src/vespa/vespalib/net/tls/crypto_codec.h
+++ b/vespalib/src/vespa/vespalib/net/tls/crypto_codec.h
@@ -1,6 +1,7 @@
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
#pragma once
+#include "capability_set.h"
#include <vespa/vespalib/net/socket_address.h>
#include <memory>
@@ -54,7 +55,6 @@ struct DecodeResult {
struct TlsContext;
struct PeerCredentials;
-class AssumedRoles;
// TODO move to different namespace, not dependent on TLS?
@@ -185,9 +185,9 @@ public:
[[nodiscard]] virtual const PeerCredentials& peer_credentials() const noexcept = 0;
/**
- * Union set of all assumed roles in the peer policy rules that fully matched the peer's credentials.
+ * Union set of all granted capabilities in the peer policy rules that fully matched the peer's credentials.
*/
- [[nodiscard]] virtual const AssumedRoles& assumed_roles() const noexcept = 0;
+ [[nodiscard]] virtual CapabilitySet granted_capabilities() const noexcept = 0;
/*
* Creates an implementation defined CryptoCodec that provides at least TLSv1.2
diff --git a/vespalib/src/vespa/vespalib/net/tls/crypto_codec_adapter.cpp b/vespalib/src/vespa/vespalib/net/tls/crypto_codec_adapter.cpp
index 03170ae0f68..a50acc55bd0 100644
--- a/vespalib/src/vespa/vespalib/net/tls/crypto_codec_adapter.cpp
+++ b/vespalib/src/vespa/vespalib/net/tls/crypto_codec_adapter.cpp
@@ -1,6 +1,7 @@
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
#include "crypto_codec_adapter.h"
+#include <vespa/vespalib/net/connection_auth_context.h>
#include <assert.h>
namespace vespalib::net::tls {
@@ -208,4 +209,10 @@ CryptoCodecAdapter::drop_empty_buffers()
_output.drop_if_empty();
}
+std::unique_ptr<net::ConnectionAuthContext>
+CryptoCodecAdapter::make_auth_context()
+{
+ return std::make_unique<net::ConnectionAuthContext>(_codec->peer_credentials(), _codec->granted_capabilities());
+}
+
} // namespace vespalib::net::tls
diff --git a/vespalib/src/vespa/vespalib/net/tls/crypto_codec_adapter.h b/vespalib/src/vespa/vespalib/net/tls/crypto_codec_adapter.h
index 1d5b57dc9b2..4b3c66cbc3c 100644
--- a/vespalib/src/vespa/vespalib/net/tls/crypto_codec_adapter.h
+++ b/vespalib/src/vespa/vespalib/net/tls/crypto_codec_adapter.h
@@ -46,6 +46,7 @@ public:
ssize_t flush() override;
ssize_t half_close() override;
void drop_empty_buffers() override;
+ std::unique_ptr<net::ConnectionAuthContext> make_auth_context() override;
};
} // namespace vespalib::net::tls
diff --git a/vespalib/src/vespa/vespalib/net/tls/impl/openssl_crypto_codec_impl.h b/vespalib/src/vespa/vespalib/net/tls/impl/openssl_crypto_codec_impl.h
index 5be2146b349..ca7237bfa9a 100644
--- a/vespalib/src/vespa/vespalib/net/tls/impl/openssl_crypto_codec_impl.h
+++ b/vespalib/src/vespa/vespalib/net/tls/impl/openssl_crypto_codec_impl.h
@@ -4,8 +4,8 @@
#include <vespa/vespalib/crypto/openssl_typedefs.h>
#include <vespa/vespalib/net/socket_address.h>
#include <vespa/vespalib/net/socket_spec.h>
-#include <vespa/vespalib/net/tls/assumed_roles.h>
#include <vespa/vespalib/net/tls/crypto_codec.h>
+#include <vespa/vespalib/net/tls/capability_set.h>
#include <vespa/vespalib/net/tls/peer_credentials.h>
#include <vespa/vespalib/net/tls/transport_security_options.h>
#include <memory>
@@ -58,7 +58,7 @@ class OpenSslCryptoCodecImpl : public CryptoCodec {
std::optional<DeferredHandshakeParams> _deferred_handshake_params;
std::optional<HandshakeResult> _deferred_handshake_result;
PeerCredentials _peer_credentials;
- AssumedRoles _assumed_roles;
+ CapabilitySet _granted_capabilities;
public:
~OpenSslCryptoCodecImpl() override;
@@ -103,8 +103,8 @@ public:
return _peer_credentials;
}
- [[nodiscard]] const AssumedRoles& assumed_roles() const noexcept override {
- return _assumed_roles;
+ [[nodiscard]] CapabilitySet granted_capabilities() const noexcept override {
+ return _granted_capabilities;
}
const SocketAddress& peer_address() const noexcept { return _peer_address; }
@@ -120,8 +120,8 @@ public:
void set_peer_credentials(PeerCredentials peer_credentials) {
_peer_credentials = std::move(peer_credentials);
}
- void set_assumed_roles(AssumedRoles assumed_roles) {
- _assumed_roles = std::move(assumed_roles);
+ void set_granted_capabilities(CapabilitySet granted_capabilities) {
+ _granted_capabilities = granted_capabilities;
}
private:
OpenSslCryptoCodecImpl(std::shared_ptr<OpenSslTlsContextImpl> ctx,
diff --git a/vespalib/src/vespa/vespalib/net/tls/impl/openssl_tls_context_impl.cpp b/vespalib/src/vespa/vespalib/net/tls/impl/openssl_tls_context_impl.cpp
index 3810140854b..d7977f6cd2a 100644
--- a/vespalib/src/vespa/vespalib/net/tls/impl/openssl_tls_context_impl.cpp
+++ b/vespalib/src/vespa/vespalib/net/tls/impl/openssl_tls_context_impl.cpp
@@ -488,7 +488,7 @@ bool OpenSslTlsContextImpl::verify_trusted_certificate(::X509_STORE_CTX* store_c
// Store away credentials and role set for later use by requests that arrive over this connection.
// TODO encapsulate as const shared_ptr to immutable object to better facilitate sharing?
codec_impl.set_peer_credentials(std::move(creds));
- codec_impl.set_assumed_roles(authz_result.steal_assumed_roles());
+ codec_impl.set_granted_capabilities(authz_result.granted_capabilities());
} catch (std::exception& e) {
LOGBT(error, codec_impl.peer_address().ip_address(),
"Got exception during certificate verification callback for peer '%s': %s",
diff --git a/vespalib/src/vespa/vespalib/net/tls/peer_policies.cpp b/vespalib/src/vespa/vespalib/net/tls/peer_policies.cpp
index a4e651f3f19..eaa6a8c2298 100644
--- a/vespalib/src/vespa/vespalib/net/tls/peer_policies.cpp
+++ b/vespalib/src/vespa/vespalib/net/tls/peer_policies.cpp
@@ -123,13 +123,14 @@ PeerPolicy::PeerPolicy() = default;
PeerPolicy::PeerPolicy(std::vector<RequiredPeerCredential> required_peer_credentials)
: _required_peer_credentials(std::move(required_peer_credentials)),
- _assumed_roles(AssumedRoles::make_wildcard_role())
-{}
+ _granted_capabilities(CapabilitySet::make_with_all_capabilities())
+{
+}
PeerPolicy::PeerPolicy(std::vector<RequiredPeerCredential> required_peer_credentials,
- AssumedRoles assumed_roles)
+ CapabilitySet granted_capabilities)
: _required_peer_credentials(std::move(required_peer_credentials)),
- _assumed_roles(std::move(assumed_roles))
+ _granted_capabilities(granted_capabilities)
{}
PeerPolicy::~PeerPolicy() = default;
@@ -170,7 +171,7 @@ std::ostream& operator<<(std::ostream& os, const RequiredPeerCredential& cred) {
std::ostream& operator<<(std::ostream& os, const PeerPolicy& policy) {
os << "PeerPolicy(";
print_joined(os, policy.required_peer_credentials(), ", ");
- os << ")";
+ os << ", " << policy.granted_capabilities().to_string() << ")";
return os;
}
diff --git a/vespalib/src/vespa/vespalib/net/tls/peer_policies.h b/vespalib/src/vespa/vespalib/net/tls/peer_policies.h
index 6eab8c2c9b2..3314e5e4adf 100644
--- a/vespalib/src/vespa/vespalib/net/tls/peer_policies.h
+++ b/vespalib/src/vespa/vespalib/net/tls/peer_policies.h
@@ -1,7 +1,7 @@
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
#pragma once
-#include "assumed_roles.h"
+#include "capability_set.h"
#include <vespa/vespalib/stllike/string.h>
#include <memory>
#include <vector>
@@ -50,26 +50,26 @@ public:
class PeerPolicy {
// _All_ credentials must match for the policy itself to match.
std::vector<RequiredPeerCredential> _required_peer_credentials;
- AssumedRoles _assumed_roles;
+ CapabilitySet _granted_capabilities;
public:
PeerPolicy();
- // This policy is created with a wildcard role set, i.e. full access.
+ // This policy is created with a full capability set, i.e. unrestricted access.
explicit PeerPolicy(std::vector<RequiredPeerCredential> required_peer_credentials);
PeerPolicy(std::vector<RequiredPeerCredential> required_peer_credentials,
- AssumedRoles assumed_roles);
+ CapabilitySet granted_capabilities);
~PeerPolicy();
bool operator==(const PeerPolicy& rhs) const noexcept {
return ((_required_peer_credentials == rhs._required_peer_credentials) &&
- (_assumed_roles == rhs._assumed_roles));
+ (_granted_capabilities == rhs._granted_capabilities));
}
[[nodiscard]] const std::vector<RequiredPeerCredential>& required_peer_credentials() const noexcept {
return _required_peer_credentials;
}
- [[nodiscard]] const AssumedRoles& assumed_roles() const noexcept {
- return _assumed_roles;
+ [[nodiscard]] const CapabilitySet& granted_capabilities() const noexcept {
+ return _granted_capabilities;
}
};
diff --git a/vespalib/src/vespa/vespalib/net/tls/policy_checking_certificate_verifier.cpp b/vespalib/src/vespa/vespalib/net/tls/policy_checking_certificate_verifier.cpp
index 4018e20225e..d9dbca6e808 100644
--- a/vespalib/src/vespa/vespalib/net/tls/policy_checking_certificate_verifier.cpp
+++ b/vespalib/src/vespa/vespalib/net/tls/policy_checking_certificate_verifier.cpp
@@ -71,16 +71,16 @@ PolicyConfiguredCertificateVerifier::~PolicyConfiguredCertificateVerifier() = de
VerificationResult PolicyConfiguredCertificateVerifier::verify(const PeerCredentials& peer_creds) const {
if (_authorized_peers.allows_all_authenticated()) {
- return VerificationResult::make_authorized_for_all_roles();
+ return VerificationResult::make_authorized_with_all_capabilities();
}
- AssumedRolesBuilder roles_builder;
+ CapabilitySet caps;
for (const auto& policy : _authorized_peers.peer_policies()) {
if (matches_all_policy_requirements(peer_creds, policy)) {
- roles_builder.add_union(policy.assumed_roles());
+ caps.add_all(policy.granted_capabilities());
}
}
- if (!roles_builder.empty()) {
- return VerificationResult::make_authorized_for_roles(roles_builder.build_with_move());
+ if (!caps.empty()) {
+ return VerificationResult::make_authorized_with_capabilities(std::move(caps));
} else {
return VerificationResult::make_not_authorized();
}
diff --git a/vespalib/src/vespa/vespalib/net/tls/tls_crypto_socket.h b/vespalib/src/vespa/vespalib/net/tls/tls_crypto_socket.h
index 0d036fdad4b..01d20155bd1 100644
--- a/vespalib/src/vespa/vespalib/net/tls/tls_crypto_socket.h
+++ b/vespalib/src/vespa/vespalib/net/tls/tls_crypto_socket.h
@@ -7,7 +7,7 @@
namespace vespalib {
struct TlsCryptoSocket : public CryptoSocket {
- ~TlsCryptoSocket();
+ ~TlsCryptoSocket() override;
virtual void inject_read_data(const char *buf, size_t len) = 0;
};
diff --git a/vespalib/src/vespa/vespalib/net/tls/transport_security_options_reading.cpp b/vespalib/src/vespa/vespalib/net/tls/transport_security_options_reading.cpp
index 2e80135813d..94281d3ef41 100644
--- a/vespalib/src/vespa/vespalib/net/tls/transport_security_options_reading.cpp
+++ b/vespalib/src/vespa/vespalib/net/tls/transport_security_options_reading.cpp
@@ -5,6 +5,8 @@
#include <vespa/vespalib/io/fileutil.h>
#include <vespa/vespalib/io/mapped_file_input.h>
#include <vespa/vespalib/data/memory_input.h>
+#include <vespa/vespalib/net/tls/capability_set.h>
+#include <vespa/vespalib/stllike/hash_set.h>
namespace vespalib::net::tls {
@@ -24,7 +26,8 @@ namespace vespalib::net::tls {
{ "field":"CN", "must-match": "*.config.blarg"},
{ "field":"SAN_DNS", "must-match": "*.fancy.config.blarg"}
],
- "name": "funky config servers"
+ "name": "funky config servers",
+ "capabilities": ["vespa.content.coolstuff"]
}
]
}
@@ -68,8 +71,7 @@ RequiredPeerCredential parse_peer_credential(const Inspector& req_entry) {
return RequiredPeerCredential(field, std::move(match));
}
-PeerPolicy parse_peer_policy(const Inspector& peer_entry) {
- auto& creds = peer_entry["required-credentials"];
+std::vector<RequiredPeerCredential> parse_peer_credentials(const Inspector& creds) {
if (creds.children() == 0) {
throw IllegalArgumentException("\"required-credentials\" array can't be empty (would allow all peers)");
}
@@ -77,7 +79,31 @@ PeerPolicy parse_peer_policy(const Inspector& peer_entry) {
for (size_t i = 0; i < creds.children(); ++i) {
required_creds.emplace_back(parse_peer_credential(creds[i]));
}
- return PeerPolicy(std::move(required_creds));
+ return required_creds;
+}
+
+CapabilitySet parse_capabilities(const Inspector& caps) {
+ CapabilitySet capabilities;
+ if (caps.valid() && (caps.children() == 0)) {
+ throw IllegalArgumentException("\"capabilities\" array must either be not present (implies "
+ "all capabilities) or contain at least one capability name");
+ } else if (caps.valid()) {
+ for (size_t i = 0; i < caps.children(); ++i) {
+ // TODO warn if resolve_and_add returns false; means capability is unknown!
+ (void)capabilities.resolve_and_add(caps[i].asString().make_string());
+ }
+ } else {
+ // If no capabilities are specified, all are implicitly granted.
+ // This avoids breaking every legacy mTLS app ever.
+ capabilities = CapabilitySet::make_with_all_capabilities();
+ }
+ return capabilities;
+}
+
+PeerPolicy parse_peer_policy(const Inspector& peer_entry) {
+ auto required_creds = parse_peer_credentials(peer_entry["required-credentials"]);
+ auto capabilities = parse_capabilities(peer_entry["capabilities"]);
+ return {std::move(required_creds), std::move(capabilities)};
}
AuthorizedPeers parse_authorized_peers(const Inspector& authorized_peers) {
diff --git a/vespalib/src/vespa/vespalib/net/tls/verification_result.cpp b/vespalib/src/vespa/vespalib/net/tls/verification_result.cpp
index e4833f59f47..f1e50d3115e 100644
--- a/vespalib/src/vespa/vespalib/net/tls/verification_result.cpp
+++ b/vespalib/src/vespa/vespalib/net/tls/verification_result.cpp
@@ -8,8 +8,8 @@ namespace vespalib::net::tls {
VerificationResult::VerificationResult() = default;
-VerificationResult::VerificationResult(AssumedRoles assumed_roles)
- : _assumed_roles(std::move(assumed_roles))
+VerificationResult::VerificationResult(CapabilitySet granted_capabilities)
+ : _granted_capabilities(std::move(granted_capabilities))
{}
VerificationResult::VerificationResult(const VerificationResult&) = default;
@@ -23,19 +23,19 @@ void VerificationResult::print(asciistream& os) const {
if (!success()) {
os << "NOT AUTHORIZED";
} else {
- os << _assumed_roles;
+ os << _granted_capabilities;
}
os << ')';
}
VerificationResult
-VerificationResult::make_authorized_for_roles(AssumedRoles assumed_roles) {
- return VerificationResult(std::move(assumed_roles));
+VerificationResult::make_authorized_with_capabilities(CapabilitySet granted_capabilities) {
+ return VerificationResult(std::move(granted_capabilities));
}
VerificationResult
-VerificationResult::make_authorized_for_all_roles() {
- return VerificationResult(AssumedRoles::make_wildcard_role());
+VerificationResult::make_authorized_with_all_capabilities() {
+ return VerificationResult(CapabilitySet::make_with_all_capabilities());
}
VerificationResult
diff --git a/vespalib/src/vespa/vespalib/net/tls/verification_result.h b/vespalib/src/vespa/vespalib/net/tls/verification_result.h
index 2de89269ba4..92b32ad92f7 100644
--- a/vespalib/src/vespa/vespalib/net/tls/verification_result.h
+++ b/vespalib/src/vespa/vespalib/net/tls/verification_result.h
@@ -1,7 +1,7 @@
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
#pragma once
-#include "assumed_roles.h"
+#include "capability_set.h"
#include <vespa/vespalib/stllike/string.h>
#include <iosfwd>
@@ -13,14 +13,14 @@ namespace vespalib::net::tls {
* The result of evaluating configured mTLS authorization rules against the
* credentials presented by a successfully authenticated peer certificate.
*
- * This result contains the union set of all roles specified by the matching
- * authorization rules. If no rules matched, the set will be empty. The role
+ * This result contains the union set of all capabilities granted by the matching
+ * authorization rules. If no rules matched, the set will be empty. The capability
* set will also be empty for a default-constructed instance.
*/
class VerificationResult {
- AssumedRoles _assumed_roles;
+ CapabilitySet _granted_capabilities;
- explicit VerificationResult(AssumedRoles assumed_roles);
+ explicit VerificationResult(CapabilitySet granted_capabilities);
public:
VerificationResult();
VerificationResult(const VerificationResult&);
@@ -29,22 +29,19 @@ public:
VerificationResult& operator=(VerificationResult&&) noexcept;
~VerificationResult();
- // Returns true iff at least one assumed role has been granted.
+ // Returns true iff at least one capability been granted.
[[nodiscard]] bool success() const noexcept {
- return !_assumed_roles.empty();
+ return !_granted_capabilities.empty();
}
- [[nodiscard]] const AssumedRoles& assumed_roles() const noexcept {
- return _assumed_roles;
- }
- [[nodiscard]] AssumedRoles steal_assumed_roles() noexcept {
- return std::move(_assumed_roles);
+ [[nodiscard]] const CapabilitySet& granted_capabilities() const noexcept {
+ return _granted_capabilities;
}
void print(asciistream& os) const;
- static VerificationResult make_authorized_for_roles(AssumedRoles assumed_roles);
- static VerificationResult make_authorized_for_all_roles();
+ static VerificationResult make_authorized_with_capabilities(CapabilitySet granted_capabilities);
+ static VerificationResult make_authorized_with_all_capabilities();
static VerificationResult make_not_authorized();
};
diff --git a/vespalib/src/vespa/vespalib/test/peer_policy_utils.cpp b/vespalib/src/vespa/vespalib/test/peer_policy_utils.cpp
index 82d7b9ea07b..c139b1391e0 100644
--- a/vespalib/src/vespa/vespalib/test/peer_policy_utils.cpp
+++ b/vespalib/src/vespa/vespalib/test/peer_policy_utils.cpp
@@ -16,25 +16,29 @@ RequiredPeerCredential required_san_uri(vespalib::stringref pattern) {
return {RequiredPeerCredential::Field::SAN_URI, pattern};
}
-AssumedRoles assumed_roles(const std::vector<string>& roles) {
- // TODO fix hash_set iterator range ctor to make this a one-liner
- AssumedRoles::RoleSet role_set;
- for (const auto& role : roles) {
- role_set.insert(role);
- }
- return AssumedRoles::make_for_roles(std::move(role_set));
-}
-
PeerPolicy policy_with(std::vector<RequiredPeerCredential> creds) {
return PeerPolicy(std::move(creds));
}
-PeerPolicy policy_with(std::vector<RequiredPeerCredential> creds, AssumedRoles roles) {
- return {std::move(creds), std::move(roles)};
+PeerPolicy policy_with(std::vector<RequiredPeerCredential> creds, CapabilitySet capabilities) {
+ return {std::move(creds), std::move(capabilities)};
}
AuthorizedPeers authorized_peers(std::vector<PeerPolicy> peer_policies) {
return AuthorizedPeers(std::move(peer_policies));
}
+Capability cap_1() {
+ return Capability::content_search_api();
+}
+Capability cap_2() {
+ return Capability::content_storage_api();
+}
+Capability cap_3() {
+ return Capability::content_document_api();
+}
+Capability cap_4() {
+ return Capability::slobrok_api();
+}
+
}
diff --git a/vespalib/src/vespa/vespalib/test/peer_policy_utils.h b/vespalib/src/vespa/vespalib/test/peer_policy_utils.h
index 72e9fde20de..5c6a97cc2c3 100644
--- a/vespalib/src/vespa/vespalib/test/peer_policy_utils.h
+++ b/vespalib/src/vespa/vespalib/test/peer_policy_utils.h
@@ -2,15 +2,20 @@
#pragma once
#include <vespa/vespalib/net/tls/peer_policies.h>
+#include <vespa/vespalib/net/tls/capability_set.h>
namespace vespalib::net::tls {
RequiredPeerCredential required_cn(vespalib::stringref pattern);
RequiredPeerCredential required_san_dns(vespalib::stringref pattern);
RequiredPeerCredential required_san_uri(vespalib::stringref pattern);
-AssumedRoles assumed_roles(const std::vector<string>& roles);
PeerPolicy policy_with(std::vector<RequiredPeerCredential> creds);
-PeerPolicy policy_with(std::vector<RequiredPeerCredential> creds, AssumedRoles roles);
+PeerPolicy policy_with(std::vector<RequiredPeerCredential> creds, CapabilitySet capabilities);
AuthorizedPeers authorized_peers(std::vector<PeerPolicy> peer_policies);
+// Some shortcuts for valid capabilities:
+Capability cap_1();
+Capability cap_2();
+Capability cap_3();
+Capability cap_4();
}