diff --git a/enterprise/authentication/authentication.cc b/enterprise/authentication/authentication.cc index 5f8204053..f0f042f99 100644 --- a/enterprise/authentication/authentication.cc +++ b/enterprise/authentication/authentication.cc @@ -4,6 +4,7 @@ #include "authentication_format.h" +#include // std::ranges::all_of #include // std::byte, std::size_t #include // std::uint32_t, std::uint64_t #include // std::getenv @@ -16,6 +17,7 @@ #include // std::string #include // std::string_view #include // std::unordered_map +#include // std::unordered_set #include // std::move namespace { @@ -192,6 +194,28 @@ auto admits_apikey(const std::span metadata, return false; } +auto collect_keys(const std::span metadata, + std::unordered_set &keys) -> void { + std::size_t cursor{0}; + std::uint32_t count{0}; + if (!read_u32(metadata, cursor, count)) { + return; + } + + for (std::uint32_t index{0}; index < count; index += 1) { + std::uint32_t length{0}; + if (!read_u32(metadata, cursor, length) || + metadata.size() - cursor < length) { + return; + } + + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + keys.emplace(reinterpret_cast(metadata.data() + cursor), + length); + cursor += length; + } +} + } // namespace namespace sourcemeta::one { @@ -327,6 +351,62 @@ struct Authentication::Impl { return false; } + struct Audience { + bool is_public; + std::unordered_set keys; + }; + + [[nodiscard]] auto audience(const std::string_view registry_path) const + -> Audience { + Audience result{.is_public = false, .keys = {}}; + const auto governing{this->match(registry_path)}; + if (governing == 0) { + return result; + } + + const auto *policies{ + static_cast(this->policies_)}; + for (std::uint32_t index{0}; index < this->policy_count_; index += 1) { + if ((governing & (PolicySet{1} << index)) == 0) { + continue; + } + + const auto &entry{policies[index]}; + const auto type{static_cast(entry.type)}; + if (type == Type::Public) { + result.is_public = true; + return result; + } + + if (type == Type::ApiKey && entry.metadata_length > 0) { + const std::span metadata{ + this->view_->as(entry.metadata_offset), + entry.metadata_length}; + collect_keys(metadata, result.keys); + } + } + + return result; + } + + [[nodiscard]] auto + reference_permitted(const std::string_view referrer_path, + const std::string_view referent_path) const -> bool { + const auto referent{this->audience(referent_path)}; + if (referent.is_public) { + return true; + } + + const auto referrer{this->audience(referrer_path)}; + if (referrer.is_public) { + return false; + } + + return std::ranges::all_of(referrer.keys, [&referent](const auto key) { + return referent.keys.contains(key); + }); + } + // The trie section bases, resolved from the header once at construction so // that matching never re-reads it. They are typed as the internal serialized // structures and point into the memory-mapped buffer below, remaining valid @@ -357,4 +437,10 @@ auto Authentication::admits(const std::string_view registry_path, return {.allowed = this->impl_->admits(registry_path, credential)}; } +auto Authentication::reference_permitted( + const std::string_view referrer_path, + const std::string_view referent_path) const -> bool { + return this->impl_->reference_permitted(referrer_path, referent_path); +} + } // namespace sourcemeta::one diff --git a/enterprise/unit/authentication/authentication_test.cc b/enterprise/unit/authentication/authentication_test.cc index b013e1896..41bd7cb64 100644 --- a/enterprise/unit/authentication/authentication_test.cc +++ b/enterprise/unit/authentication/authentication_test.cc @@ -295,6 +295,190 @@ TEST(Authentication, public_overrides_apikey_on_shared_path) { EXPECT_TRUE(authentication.admits("/internal/foo", "").allowed); } +TEST(Authentication, reference_to_a_public_schema_is_permitted) { + const std::array open_paths{{"/open"}}; + const std::array secret_paths{{"/secret"}}; + const std::array keys{{"ONE_TEST_REF_PUBLIC"}}; + const std::array policies{ + {{sourcemeta::one::Authentication::Type::Public, open_paths}, + {sourcemeta::one::Authentication::Type::ApiKey, secret_paths, keys}}}; + const auto path{test_path("ref_to_public.bin")}; + sourcemeta::one::Authentication::save(policies, path); + + const sourcemeta::one::Authentication authentication{path}; + EXPECT_TRUE(authentication.reference_permitted("/secret/one", "/open/two")); + EXPECT_TRUE(authentication.reference_permitted("/open/one", "/open/two")); +} + +TEST(Authentication, public_schema_referencing_an_apikey_schema_is_rejected) { + const std::array open_paths{{"/open"}}; + const std::array secret_paths{{"/secret"}}; + const std::array keys{{"ONE_TEST_REF_LEAK"}}; + const std::array policies{ + {{sourcemeta::one::Authentication::Type::Public, open_paths}, + {sourcemeta::one::Authentication::Type::ApiKey, secret_paths, keys}}}; + const auto path{test_path("ref_public_to_apikey.bin")}; + sourcemeta::one::Authentication::save(policies, path); + + const sourcemeta::one::Authentication authentication{path}; + EXPECT_FALSE(authentication.reference_permitted("/open/one", "/secret/two")); +} + +TEST(Authentication, reference_within_the_same_policy_is_permitted) { + const std::array paths{{"/internal"}}; + const std::array keys{{"ONE_TEST_REF_SAME"}}; + const std::array policies{ + {{sourcemeta::one::Authentication::Type::ApiKey, paths, keys}}}; + const auto path{test_path("ref_same_policy.bin")}; + sourcemeta::one::Authentication::save(policies, path); + + const sourcemeta::one::Authentication authentication{path}; + EXPECT_TRUE( + authentication.reference_permitted("/internal/one", "/internal/two")); + EXPECT_TRUE( + authentication.reference_permitted("/internal/one", "/internal/one")); +} + +TEST(Authentication, reference_across_disjoint_policies_is_rejected) { + const std::array alpha_paths{{"/alpha"}}; + const std::array beta_paths{{"/beta"}}; + const std::array alpha_keys{{"ONE_TEST_REF_ALPHA"}}; + const std::array beta_keys{{"ONE_TEST_REF_BETA"}}; + const std::array policies{ + {{sourcemeta::one::Authentication::Type::ApiKey, alpha_paths, alpha_keys}, + {sourcemeta::one::Authentication::Type::ApiKey, beta_paths, beta_keys}}}; + const auto path{test_path("ref_disjoint.bin")}; + sourcemeta::one::Authentication::save(policies, path); + + const sourcemeta::one::Authentication authentication{path}; + EXPECT_FALSE(authentication.reference_permitted("/alpha/one", "/beta/two")); + EXPECT_FALSE(authentication.reference_permitted("/beta/two", "/alpha/one")); +} + +TEST(Authentication, + reference_from_a_narrower_to_a_wider_audience_is_permitted) { + const std::array broad_paths{{"/p"}}; + const std::array nested_paths{{"/p/inner"}}; + const std::array broad_keys{{"ONE_TEST_REF_BROAD"}}; + const std::array nested_keys{{"ONE_TEST_REF_NESTED"}}; + const std::array policies{ + {{sourcemeta::one::Authentication::Type::ApiKey, broad_paths, broad_keys}, + {sourcemeta::one::Authentication::Type::ApiKey, nested_paths, + nested_keys}}}; + const auto path{test_path("ref_narrow_to_wide.bin")}; + sourcemeta::one::Authentication::save(policies, path); + + const sourcemeta::one::Authentication authentication{path}; + EXPECT_TRUE(authentication.reference_permitted("/p/one", "/p/inner/two")); + EXPECT_FALSE(authentication.reference_permitted("/p/inner/two", "/p/one")); +} + +TEST(Authentication, sharing_one_policy_is_insufficient) { + const std::array broad_paths{{"/p"}}; + const std::array alpha_paths{{"/p/alpha"}}; + const std::array gamma_paths{{"/p/gamma"}}; + const std::array broad_keys{{"ONE_TEST_SHARE_BROAD"}}; + const std::array alpha_keys{{"ONE_TEST_SHARE_ALPHA"}}; + const std::array gamma_keys{{"ONE_TEST_SHARE_GAMMA"}}; + const std::array policies{ + {{sourcemeta::one::Authentication::Type::ApiKey, broad_paths, broad_keys}, + {sourcemeta::one::Authentication::Type::ApiKey, alpha_paths, alpha_keys}, + {sourcemeta::one::Authentication::Type::ApiKey, gamma_paths, + gamma_keys}}}; + const auto path{test_path("ref_shared_policy.bin")}; + sourcemeta::one::Authentication::save(policies, path); + + const sourcemeta::one::Authentication authentication{path}; + EXPECT_FALSE( + authentication.reference_permitted("/p/alpha/one", "/p/gamma/two")); + EXPECT_FALSE( + authentication.reference_permitted("/p/gamma/two", "/p/alpha/one")); +} + +TEST(Authentication, reference_from_an_ungoverned_schema_is_permitted) { + const std::array paths{{"/secret"}}; + const std::array keys{{"ONE_TEST_REF_UNGOVERNED"}}; + const std::array policies{ + {{sourcemeta::one::Authentication::Type::ApiKey, paths, keys}}}; + const auto path{test_path("ref_ungoverned_from.bin")}; + sourcemeta::one::Authentication::save(policies, path); + + const sourcemeta::one::Authentication authentication{path}; + EXPECT_TRUE( + authentication.reference_permitted("/nowhere/one", "/secret/two")); +} + +TEST(Authentication, reference_to_an_ungoverned_schema_is_rejected) { + const std::array open_paths{{"/open"}}; + const std::array secret_paths{{"/secret"}}; + const std::array keys{{"ONE_TEST_REF_TO_DENY"}}; + const std::array policies{ + {{sourcemeta::one::Authentication::Type::Public, open_paths}, + {sourcemeta::one::Authentication::Type::ApiKey, secret_paths, keys}}}; + const auto path{test_path("ref_to_ungoverned.bin")}; + sourcemeta::one::Authentication::save(policies, path); + + const sourcemeta::one::Authentication authentication{path}; + EXPECT_FALSE(authentication.reference_permitted("/open/one", "/nowhere/two")); + EXPECT_FALSE( + authentication.reference_permitted("/secret/one", "/nowhere/two")); +} + +TEST(Authentication, public_carveout_under_an_apikey_prefix) { + const std::array private_paths{{"/private"}}; + const std::array open_paths{{"/private/open"}}; + const std::array keys{{"ONE_TEST_CARVEOUT"}}; + const std::array policies{ + {{sourcemeta::one::Authentication::Type::ApiKey, private_paths, keys}, + {sourcemeta::one::Authentication::Type::Public, open_paths}}}; + const auto path{test_path("ref_carveout.bin")}; + sourcemeta::one::Authentication::save(policies, path); + + const sourcemeta::one::Authentication authentication{path}; + EXPECT_TRUE(authentication.reference_permitted("/private/secret", + "/private/open/shared")); + EXPECT_FALSE(authentication.reference_permitted("/private/open/shared", + "/private/secret")); +} + +TEST(Authentication, reference_honours_shared_environment_variable_names) { + const std::array alpha_paths{{"/alpha"}}; + const std::array beta_paths{{"/beta"}}; + const std::array shared_keys{{"ONE_TEST_REF_SHARED"}}; + const std::array policies{ + {{sourcemeta::one::Authentication::Type::ApiKey, alpha_paths, + shared_keys}, + {sourcemeta::one::Authentication::Type::ApiKey, beta_paths, + shared_keys}}}; + const auto path{test_path("ref_shared_variable.bin")}; + sourcemeta::one::Authentication::save(policies, path); + + const sourcemeta::one::Authentication authentication{path}; + EXPECT_TRUE(authentication.reference_permitted("/alpha/one", "/beta/two")); + EXPECT_TRUE(authentication.reference_permitted("/beta/two", "/alpha/one")); +} + +TEST(Authentication, reference_distinguishes_sibling_prefix_policies) { + const std::array public_paths{{"/public"}}; + const std::array apikey_paths{{"/pub"}}; + const std::array keys{{"ONE_TEST_SIBLING_KEY"}}; + const std::array policies{ + {{sourcemeta::one::Authentication::Type::Public, public_paths}, + {sourcemeta::one::Authentication::Type::ApiKey, apikey_paths, keys}}}; + const auto path{test_path("ref_sibling_prefix.bin")}; + sourcemeta::one::Authentication::save(policies, path); + + const sourcemeta::one::Authentication authentication{path}; + EXPECT_FALSE(authentication.reference_permitted("/public/a", "/pub/b")); + EXPECT_TRUE(authentication.reference_permitted("/pub/b", "/public/a")); +} + +TEST(Authentication, reference_permitted_on_a_missing_artifact_is_permitted) { + const sourcemeta::one::Authentication authentication{ + std::filesystem::path{"/no/such/authentication.bin"}}; + EXPECT_TRUE(authentication.reference_permitted("/a/one", "/b/two")); +} + TEST(Authentication, metadata_outside_its_region_denies_everything) { setenv("ONE_TEST_KEY_REGION", "region-secret", 1); const std::array public_paths{{"/public"}}; diff --git a/src/authentication/authentication.cc b/src/authentication/authentication.cc index 4801a211d..46cffbcb1 100644 --- a/src/authentication/authentication.cc +++ b/src/authentication/authentication.cc @@ -28,4 +28,9 @@ auto Authentication::admits(const std::string_view, return {.allowed = true}; } +auto Authentication::reference_permitted(const std::string_view, + const std::string_view) const -> bool { + return true; +} + } // namespace sourcemeta::one diff --git a/src/authentication/include/sourcemeta/one/authentication.h b/src/authentication/include/sourcemeta/one/authentication.h index 00b16cb16..59ff2ebf4 100644 --- a/src/authentication/include/sourcemeta/one/authentication.h +++ b/src/authentication/include/sourcemeta/one/authentication.h @@ -45,6 +45,10 @@ class SOURCEMETA_ONE_AUTHENTICATION_EXPORT Authentication { [[nodiscard]] auto admits(std::string_view registry_path, std::string_view credential) const -> Verdict; + [[nodiscard]] auto reference_permitted(std::string_view referrer_path, + std::string_view referent_path) const + -> bool; + private: // The implementation differs by edition and owns the memory-mapped artifact, // so it is hidden behind a pointer to keep the binary format out of the diff --git a/src/index/error.h b/src/index/error.h index b2a7b18e0..ccea3647b 100644 --- a/src/index/error.h +++ b/src/index/error.h @@ -104,6 +104,29 @@ class EnterpriseOnlyFeatureError : public std::exception { const char *message_; }; +class CrossPolicyReferenceError : public std::exception { +public: + CrossPolicyReferenceError(std::string referrer, std::string referent) + : referrer_{std::move(referrer)}, referent_{std::move(referent)} {} + + [[nodiscard]] auto what() const noexcept -> const char * override { + return "A schema cannot reference a schema behind a stricter " + "authentication policy"; + } + + [[nodiscard]] auto referrer() const noexcept -> const std::string & { + return this->referrer_; + } + + [[nodiscard]] auto referent() const noexcept -> const std::string & { + return this->referent_; + } + +private: + std::string referrer_; + std::string referent_; +}; + } // namespace sourcemeta::one #endif diff --git a/src/index/generators.h b/src/index/generators.h index 8166abe68..d50ebfaf1 100644 --- a/src/index/generators.h +++ b/src/index/generators.h @@ -37,10 +37,13 @@ #include // std::numeric_limits #include // std::unique_ptr #include // std::mutex, std::lock_guard +#include // std::optional #include // std::ostream #include // std::queue #include // std::set #include // std::ostringstream +#include // std::string +#include // std::string_view #include // std::tuple #include // std::unordered_map #include // std::move, std::pair @@ -259,7 +262,7 @@ struct GENERATE_DEPENDENCIES { const sourcemeta::one::BuildPlan::Action &action, const sourcemeta::one::BuildDynamicCallback &callback, sourcemeta::one::Resolver &resolver, - const sourcemeta::one::Configuration &, + const sourcemeta::one::Configuration &configuration, const sourcemeta::core::JSON &) -> void { const auto timestamp_start{std::chrono::steady_clock::now()}; const auto contents_option{ @@ -283,6 +286,23 @@ struct GENERATE_DEPENDENCIES { }); // Otherwise we are returning non-sense assert(result.unique()); + + if (result.size() > 0) { + const sourcemeta::one::Authentication authentication{ + action.dependencies.at(1)}; + for (const auto &edge : result.as_array()) { + const auto &referrer_uri{edge.at("from").to_string()}; + const auto &referent_uri{edge.at("to").to_string()}; + const auto referrer{registry_path(referrer_uri, configuration.url)}; + const auto referent{registry_path(referent_uri, configuration.url)}; + if (referrer.has_value() && referent.has_value() && + !authentication.reference_permitted(referrer.value(), + referent.value())) { + throw CrossPolicyReferenceError(referrer_uri, referent_uri); + } + } + } + const auto timestamp_end{std::chrono::steady_clock::now()}; sourcemeta::one::metapack_write_pretty_json( @@ -301,6 +321,22 @@ struct GENERATE_DEPENDENCIES { return sourcemeta::core::JSON{uri}; } + + static auto registry_path(const std::string_view uri, + const std::string_view base) + -> std::optional { + if (!uri.starts_with(base)) { + return std::nullopt; + } + + const auto remainder{uri.substr(base.size())}; + if (!base.ends_with('/') && !remainder.empty() && + !remainder.starts_with('/')) { + return std::nullopt; + } + + return remainder; + } }; // The relevant input dependencies files are determined by delta. The handler diff --git a/src/index/index.cc b/src/index/index.cc index 694032510..3a618687c 100644 --- a/src/index/index.cc +++ b/src/index/index.cc @@ -724,6 +724,10 @@ auto main(int argc, char *argv[]) noexcept -> int { std::print(stdout, "error: {}\n at path {}\n", error.what(), error.path().string()); return EXIT_FAILURE; + } catch (const sourcemeta::one::CrossPolicyReferenceError &error) { + std::print(stdout, "error: {}\n at schema {}\n with reference {}\n", + error.what(), error.referrer(), error.referent()); + return EXIT_FAILURE; } catch (const sourcemeta::core::FileError< sourcemeta::blaze::SchemaRuleInvalidNamePatternError> &error) { std::print(stdout, diff --git a/src/index/rules.h b/src/index/rules.h index a3006972c..eabec8fff 100644 --- a/src/index/rules.h +++ b/src/index/rules.h @@ -94,8 +94,11 @@ inline constexpr DeltaRuleSet<13, 6, 5, 2> INDEX_RULES{ .tracks_dependencies = true, .dependencies = {{{.source = DependencySource::Base, .base = 0, - .filename = "schema.metapack"}}}, - .dependency_count = 1}, + .filename = "schema.metapack"}, + {.source = DependencySource::GlobalOutput, + .base = 0, + .filename = "authentication.bin"}}}, + .dependency_count = 2}, {.action = ACTION_STATS, .base = 0, diff --git a/test/cli/CMakeLists.txt b/test/cli/CMakeLists.txt index 898a26984..0668b85c4 100644 --- a/test/cli/CMakeLists.txt +++ b/test/cli/CMakeLists.txt @@ -171,6 +171,12 @@ if(ONE_INDEX) sourcemeta_one_test_cli(common index snapshot-no-self-yaml-schemas) if(ONE_ENTERPRISE) + sourcemeta_one_test_cli(enterprise index fail-cross-policy-reference) + sourcemeta_one_test_cli(enterprise index fail-cross-policy-disjoint-keys) + sourcemeta_one_test_cli(enterprise index fail-cross-policy-shared-prefix) + sourcemeta_one_test_cli(enterprise index fail-cross-policy-public-carve-out) + sourcemeta_one_test_cli(enterprise index fail-cross-policy-nested-scope) + sourcemeta_one_test_cli(enterprise index fail-cross-policy-prefix-sibling) sourcemeta_one_test_cli(enterprise index fail-lint-rule-invalid-title) sourcemeta_one_test_cli(enterprise index fail-lint-rule-no-dialect) sourcemeta_one_test_cli(enterprise index fail-lint-rule-no-title) diff --git a/test/cli/index/enterprise/fail-cross-policy-disjoint-keys.sh b/test/cli/index/enterprise/fail-cross-policy-disjoint-keys.sh new file mode 100755 index 000000000..89f68a9a3 --- /dev/null +++ b/test/cli/index/enterprise/fail-cross-policy-disjoint-keys.sh @@ -0,0 +1,61 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << EOF > "$TMP/one.json" +{ + "url": "http://localhost:8000", + "authentication": [ + { + "type": "apiKey", + "algorithm": "identity", + "paths": [ "/alpha" ], + "keys": [ { "environmentVariable": "ONE_TEST_KEY_ALPHA" } ] + }, + { + "type": "apiKey", + "algorithm": "identity", + "paths": [ "/beta" ], + "keys": [ { "environmentVariable": "ONE_TEST_KEY_BETA" } ] + } + ], + "contents": { + "alpha": { "path": "./alpha" }, + "beta": { "path": "./beta" } + } +} +EOF + +mkdir "$TMP/alpha" +mkdir "$TMP/beta" + +cat << 'EOF' > "$TMP/beta/two.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object" +} +EOF + +cat << 'EOF' > "$TMP/alpha/one.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { "other": { "$ref": "../beta/two" } } +} +EOF + +"$1" --skip-banner --concurrency 1 "$TMP/one.json" "$TMP/output" \ + > "$TMP/output.txt" 2>/dev/null && CODE="$?" || CODE="$?" +test "$CODE" = "1" || exit 1 + +cat << EOF > "$TMP/expected.txt" +error: A schema cannot reference a schema behind a stricter authentication policy + at schema http://localhost:8000/alpha/one + with reference http://localhost:8000/beta/two +EOF + +diff "$TMP/output.txt" "$TMP/expected.txt" diff --git a/test/cli/index/enterprise/fail-cross-policy-nested-scope.sh b/test/cli/index/enterprise/fail-cross-policy-nested-scope.sh new file mode 100755 index 000000000..ee4eb1add --- /dev/null +++ b/test/cli/index/enterprise/fail-cross-policy-nested-scope.sh @@ -0,0 +1,58 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << EOF > "$TMP/one.json" +{ + "url": "http://localhost:8000", + "authentication": [ + { + "type": "apiKey", + "algorithm": "identity", + "paths": [ "/p" ], + "keys": [ { "environmentVariable": "ONE_TEST_KEY_BROAD" } ] + }, + { + "type": "apiKey", + "algorithm": "identity", + "paths": [ "/p/inner" ], + "keys": [ { "environmentVariable": "ONE_TEST_KEY_INNER" } ] + } + ], + "contents": { "p": { "path": "./p" } } +} +EOF + +mkdir "$TMP/p" +mkdir "$TMP/p/inner" + +cat << 'EOF' > "$TMP/p/shallow.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object" +} +EOF + +cat << 'EOF' > "$TMP/p/inner/deep.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { "other": { "$ref": "../shallow" } } +} +EOF + +"$1" --skip-banner --concurrency 1 "$TMP/one.json" "$TMP/output" \ + > "$TMP/output.txt" 2>/dev/null && CODE="$?" || CODE="$?" +test "$CODE" = "1" || exit 1 + +cat << EOF > "$TMP/expected.txt" +error: A schema cannot reference a schema behind a stricter authentication policy + at schema http://localhost:8000/p/inner/deep + with reference http://localhost:8000/p/shallow +EOF + +diff "$TMP/output.txt" "$TMP/expected.txt" diff --git a/test/cli/index/enterprise/fail-cross-policy-prefix-sibling.sh b/test/cli/index/enterprise/fail-cross-policy-prefix-sibling.sh new file mode 100755 index 000000000..28eeaeea2 --- /dev/null +++ b/test/cli/index/enterprise/fail-cross-policy-prefix-sibling.sh @@ -0,0 +1,56 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << EOF > "$TMP/one.json" +{ + "url": "http://localhost:8000", + "authentication": [ + { "type": "public", "paths": [ "/public" ] }, + { + "type": "apiKey", + "algorithm": "identity", + "paths": [ "/pub" ], + "keys": [ { "environmentVariable": "ONE_TEST_KEY_PUB" } ] + } + ], + "contents": { + "public": { "path": "./public" }, + "pub": { "path": "./pub" } + } +} +EOF + +mkdir "$TMP/public" +mkdir "$TMP/pub" + +cat << 'EOF' > "$TMP/pub/b.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object" +} +EOF + +cat << 'EOF' > "$TMP/public/a.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { "other": { "$ref": "../pub/b" } } +} +EOF + +"$1" --skip-banner --concurrency 1 "$TMP/one.json" "$TMP/output" \ + > "$TMP/output.txt" 2>/dev/null && CODE="$?" || CODE="$?" +test "$CODE" = "1" || exit 1 + +cat << EOF > "$TMP/expected.txt" +error: A schema cannot reference a schema behind a stricter authentication policy + at schema http://localhost:8000/public/a + with reference http://localhost:8000/pub/b +EOF + +diff "$TMP/output.txt" "$TMP/expected.txt" diff --git a/test/cli/index/enterprise/fail-cross-policy-public-carve-out.sh b/test/cli/index/enterprise/fail-cross-policy-public-carve-out.sh new file mode 100755 index 000000000..bfd5fb551 --- /dev/null +++ b/test/cli/index/enterprise/fail-cross-policy-public-carve-out.sh @@ -0,0 +1,53 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << EOF > "$TMP/one.json" +{ + "url": "http://localhost:8000", + "authentication": [ + { + "type": "apiKey", + "algorithm": "identity", + "paths": [ "/private" ], + "keys": [ { "environmentVariable": "ONE_TEST_KEY_PRIVATE" } ] + }, + { "type": "public", "paths": [ "/private/open" ] } + ], + "contents": { "private": { "path": "./private" } } +} +EOF + +mkdir "$TMP/private" +mkdir "$TMP/private/open" + +cat << 'EOF' > "$TMP/private/secret.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object" +} +EOF + +cat << 'EOF' > "$TMP/private/open/leak.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { "secret": { "$ref": "../secret" } } +} +EOF + +"$1" --skip-banner --concurrency 1 "$TMP/one.json" "$TMP/output" \ + > "$TMP/output.txt" 2>/dev/null && CODE="$?" || CODE="$?" +test "$CODE" = "1" || exit 1 + +cat << EOF > "$TMP/expected.txt" +error: A schema cannot reference a schema behind a stricter authentication policy + at schema http://localhost:8000/private/open/leak + with reference http://localhost:8000/private/secret +EOF + +diff "$TMP/output.txt" "$TMP/expected.txt" diff --git a/test/cli/index/enterprise/fail-cross-policy-reference.sh b/test/cli/index/enterprise/fail-cross-policy-reference.sh new file mode 100755 index 000000000..553eabc54 --- /dev/null +++ b/test/cli/index/enterprise/fail-cross-policy-reference.sh @@ -0,0 +1,56 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << EOF > "$TMP/one.json" +{ + "url": "http://localhost:8000", + "authentication": [ + { "type": "public", "paths": [ "/public" ] }, + { + "type": "apiKey", + "algorithm": "identity", + "paths": [ "/private" ], + "keys": [ { "environmentVariable": "ONE_TEST_CROSS_POLICY_KEY" } ] + } + ], + "contents": { + "public": { "path": "./public" }, + "private": { "path": "./private" } + } +} +EOF + +mkdir "$TMP/public" +mkdir "$TMP/private" + +cat << 'EOF' > "$TMP/private/secret.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object" +} +EOF + +cat << 'EOF' > "$TMP/public/leak.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { "secret": { "$ref": "../private/secret" } } +} +EOF + +"$1" --skip-banner --concurrency 1 "$TMP/one.json" "$TMP/output" \ + > "$TMP/output.txt" 2>/dev/null && CODE="$?" || CODE="$?" +test "$CODE" = "1" || exit 1 + +cat << EOF > "$TMP/expected.txt" +error: A schema cannot reference a schema behind a stricter authentication policy + at schema http://localhost:8000/public/leak + with reference http://localhost:8000/private/secret +EOF + +diff "$TMP/output.txt" "$TMP/expected.txt" diff --git a/test/cli/index/enterprise/fail-cross-policy-shared-prefix.sh b/test/cli/index/enterprise/fail-cross-policy-shared-prefix.sh new file mode 100755 index 000000000..fe27bbb8e --- /dev/null +++ b/test/cli/index/enterprise/fail-cross-policy-shared-prefix.sh @@ -0,0 +1,65 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << EOF > "$TMP/one.json" +{ + "url": "http://localhost:8000", + "authentication": [ + { + "type": "apiKey", + "algorithm": "identity", + "paths": [ "/p" ], + "keys": [ { "environmentVariable": "ONE_TEST_KEY_BROAD" } ] + }, + { + "type": "apiKey", + "algorithm": "identity", + "paths": [ "/p/alpha" ], + "keys": [ { "environmentVariable": "ONE_TEST_KEY_ALPHA" } ] + }, + { + "type": "apiKey", + "algorithm": "identity", + "paths": [ "/p/gamma" ], + "keys": [ { "environmentVariable": "ONE_TEST_KEY_GAMMA" } ] + } + ], + "contents": { "p": { "path": "./p" } } +} +EOF + +mkdir "$TMP/p" +mkdir "$TMP/p/alpha" +mkdir "$TMP/p/gamma" + +cat << 'EOF' > "$TMP/p/gamma/two.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object" +} +EOF + +cat << 'EOF' > "$TMP/p/alpha/one.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { "other": { "$ref": "../gamma/two" } } +} +EOF + +"$1" --skip-banner --concurrency 1 "$TMP/one.json" "$TMP/output" \ + > "$TMP/output.txt" 2>/dev/null && CODE="$?" || CODE="$?" +test "$CODE" = "1" || exit 1 + +cat << EOF > "$TMP/expected.txt" +error: A schema cannot reference a schema behind a stricter authentication policy + at schema http://localhost:8000/p/alpha/one + with reference http://localhost:8000/p/gamma/two +EOF + +diff "$TMP/output.txt" "$TMP/expected.txt" diff --git a/test/unit/authentication/authentication_test.cc b/test/unit/authentication/authentication_test.cc index be3a9f7ee..1e12d6664 100644 --- a/test/unit/authentication/authentication_test.cc +++ b/test/unit/authentication/authentication_test.cc @@ -41,3 +41,12 @@ TEST(Authentication, save_emits_an_empty_artifact_that_admits_everything) { EXPECT_TRUE(authentication.admits("/", "").allowed); EXPECT_TRUE(authentication.admits("/internal/foo", "").allowed); } + +TEST(Authentication, permits_every_reference) { + const sourcemeta::one::Authentication authentication{ + std::filesystem::path{"/no/such/authentication.bin"}}; + EXPECT_TRUE(authentication.reference_permitted("/one", "/two")); + EXPECT_TRUE( + authentication.reference_permitted("/public/one", "/private/two")); + EXPECT_TRUE(authentication.reference_permitted("/internal/a", "/internal/a")); +}