Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions enterprise/authentication/authentication.cc
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

#include "authentication_format.h"

#include <algorithm> // std::ranges::all_of
#include <cstddef> // std::byte, std::size_t
#include <cstdint> // std::uint32_t, std::uint64_t
#include <cstdlib> // std::getenv
Expand All @@ -16,6 +17,7 @@
#include <string> // std::string
#include <string_view> // std::string_view
#include <unordered_map> // std::unordered_map
#include <unordered_set> // std::unordered_set
#include <utility> // std::move

namespace {
Expand Down Expand Up @@ -192,6 +194,28 @@ auto admits_apikey(const std::span<const std::byte> metadata,
return false;
}

auto collect_keys(const std::span<const std::byte> metadata,
std::unordered_set<std::string_view> &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<const char *>(metadata.data() + cursor),
length);
cursor += length;
}
}

} // namespace

namespace sourcemeta::one {
Expand Down Expand Up @@ -327,6 +351,62 @@ struct Authentication::Impl {
return false;
}

struct Audience {
bool is_public;
std::unordered_set<std::string_view> 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<const AuthenticationPolicyEntry *>(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<Type>(entry.type)};
if (type == Type::Public) {
result.is_public = true;
return result;
}

if (type == Type::ApiKey && entry.metadata_length > 0) {
const std::span<const std::byte> metadata{
this->view_->as<std::byte>(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
Expand Down Expand Up @@ -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
184 changes: 184 additions & 0 deletions enterprise/unit/authentication/authentication_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::string_view, 1> open_paths{{"/open"}};
const std::array<std::string_view, 1> secret_paths{{"/secret"}};
const std::array<std::string_view, 1> keys{{"ONE_TEST_REF_PUBLIC"}};
const std::array<sourcemeta::one::Authentication::Policy, 2> 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<std::string_view, 1> open_paths{{"/open"}};
const std::array<std::string_view, 1> secret_paths{{"/secret"}};
const std::array<std::string_view, 1> keys{{"ONE_TEST_REF_LEAK"}};
const std::array<sourcemeta::one::Authentication::Policy, 2> 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<std::string_view, 1> paths{{"/internal"}};
const std::array<std::string_view, 1> keys{{"ONE_TEST_REF_SAME"}};
const std::array<sourcemeta::one::Authentication::Policy, 1> 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<std::string_view, 1> alpha_paths{{"/alpha"}};
const std::array<std::string_view, 1> beta_paths{{"/beta"}};
const std::array<std::string_view, 1> alpha_keys{{"ONE_TEST_REF_ALPHA"}};
const std::array<std::string_view, 1> beta_keys{{"ONE_TEST_REF_BETA"}};
const std::array<sourcemeta::one::Authentication::Policy, 2> 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<std::string_view, 1> broad_paths{{"/p"}};
const std::array<std::string_view, 1> nested_paths{{"/p/inner"}};
const std::array<std::string_view, 1> broad_keys{{"ONE_TEST_REF_BROAD"}};
const std::array<std::string_view, 1> nested_keys{{"ONE_TEST_REF_NESTED"}};
const std::array<sourcemeta::one::Authentication::Policy, 2> 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<std::string_view, 1> broad_paths{{"/p"}};
const std::array<std::string_view, 1> alpha_paths{{"/p/alpha"}};
const std::array<std::string_view, 1> gamma_paths{{"/p/gamma"}};
const std::array<std::string_view, 1> broad_keys{{"ONE_TEST_SHARE_BROAD"}};
const std::array<std::string_view, 1> alpha_keys{{"ONE_TEST_SHARE_ALPHA"}};
const std::array<std::string_view, 1> gamma_keys{{"ONE_TEST_SHARE_GAMMA"}};
const std::array<sourcemeta::one::Authentication::Policy, 3> 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<std::string_view, 1> paths{{"/secret"}};
const std::array<std::string_view, 1> keys{{"ONE_TEST_REF_UNGOVERNED"}};
const std::array<sourcemeta::one::Authentication::Policy, 1> 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<std::string_view, 1> open_paths{{"/open"}};
const std::array<std::string_view, 1> secret_paths{{"/secret"}};
const std::array<std::string_view, 1> keys{{"ONE_TEST_REF_TO_DENY"}};
const std::array<sourcemeta::one::Authentication::Policy, 2> 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<std::string_view, 1> private_paths{{"/private"}};
const std::array<std::string_view, 1> open_paths{{"/private/open"}};
const std::array<std::string_view, 1> keys{{"ONE_TEST_CARVEOUT"}};
const std::array<sourcemeta::one::Authentication::Policy, 2> 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<std::string_view, 1> alpha_paths{{"/alpha"}};
const std::array<std::string_view, 1> beta_paths{{"/beta"}};
const std::array<std::string_view, 1> shared_keys{{"ONE_TEST_REF_SHARED"}};
const std::array<sourcemeta::one::Authentication::Policy, 2> 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<std::string_view, 1> public_paths{{"/public"}};
const std::array<std::string_view, 1> apikey_paths{{"/pub"}};
const std::array<std::string_view, 1> keys{{"ONE_TEST_SIBLING_KEY"}};
const std::array<sourcemeta::one::Authentication::Policy, 2> 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<std::string_view, 1> public_paths{{"/public"}};
Expand Down
5 changes: 5 additions & 0 deletions src/authentication/authentication.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 4 additions & 0 deletions src/authentication/include/sourcemeta/one/authentication.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 23 additions & 0 deletions src/index/error.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading
Loading