diff options
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(); } |