diff options
author | Tor Brede Vekterli <vekterli@yahooinc.com> | 2022-06-22 15:44:57 +0000 |
---|---|---|
committer | Tor Brede Vekterli <vekterli@yahooinc.com> | 2022-06-29 11:20:24 +0000 |
commit | cc44b799f0d78a5e26f12ecb8b868301095570c4 (patch) | |
tree | 374f50996663fbdfa85d529202c0e7cccb99648d /vespalib | |
parent | cbe98d69506bf60f7fcf7681eb99a79589300882 (diff) |
Support mTLS connection-level capabilities and RPC access filtering in C++
Adds the following:
* Named capabilities and capability sets that represent (respectively)
a single Vespa access API (such as Document API, search API etc)
or a concrete subset of individual capabilities that make up a
particular Vespa service (such as a content node).
* A new `capabilities` array field to the mTLS authorization policies
that allows for constraining what requests sent over a particular
connection are allowed to actually do. Capabilities are referenced
by name and may include any combination of capability sets and
individual capabilities. If multiple capabilities/sets are configured,
the resulting set of capabilities is the union set of all of them.
* An FRT RPC-level access filter that can be set up as part of RPC
method definitions. If set, filters are invoked prior to RPC methods.
* A new `PERMISSION_DENIED` error code to FRT RPC that is invoked if
an access filter denies a request.
This also GCs the unused `AssumedRoles` concept which is now deprecated
in favor of capabilities.
Note: this is **not yet** a public or stable API, and capability
names/semantics may change at any time.
Diffstat (limited to 'vespalib')
33 files changed, 848 insertions, 312 deletions
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(); } |