Skip to content

Commit

Permalink
MB-47636: Add support for limits and uuid in cbsasl.json
Browse files Browse the repository at this point in the history
The rate limit properties will be provided through cbsasl.json.
This patch adds support for reading a file with the properties
specified.

The new per-user entry looks like:

    {
      "limits": {
        "egress_mib_per_min": 1,
        "ingress_mib_per_min": 1,
        "num_ops_per_min": 1,
        "num_connections": 1
      },
      "n": "username",
      "sha512": {
        "h": "base64 encoded sha512 hash of the password",
        "s": "base64 encoded salt",
        "i": iteration-count
      },
      "sha256": {
        "h": "base64 encoded sha256 hash of the password",
        "s": "base64 encoded salt",
        "i": iteration-count
      },
      "sha1": {
        "h": "base64 encoded sha1 hash of the password",
        "s": "base64 encoded salt",
        "i": iteration-count
      },
      "plain": "base64 encoded hex version of sha1 hash of plain text password",
      "uuid": "00000000-0000-0000-0000-000000000000"
    }

If no value is set for a limit it is set to 0 (unlimited)

Change-Id: I3b8bbdcbac01bf985e501e042020c004cad07216
Reviewed-on: http://review.couchbase.org/c/kv_engine/+/158413
Reviewed-by: Dave Rigby <daver@couchbase.com>
Tested-by: Build Bot <build@couchbase.com>
  • Loading branch information
trondn committed Jul 29, 2021
1 parent 7ddf11d commit 27c9335
Show file tree
Hide file tree
Showing 8 changed files with 201 additions and 127 deletions.
1 change: 0 additions & 1 deletion cbsasl/CMakeLists.txt
Expand Up @@ -30,7 +30,6 @@ add_library(cbsasl STATIC
strcmp.cc
strerror.cc
user.cc
user.h
util.h)
kv_enable_pch(cbsasl)
cb_enable_unity_build(cbsasl)
Expand Down
2 changes: 1 addition & 1 deletion cbsasl/password_database.cc
Expand Up @@ -54,7 +54,7 @@ PasswordDatabase::PasswordDatabase(const std::string& content, bool file) {

// parse all of the users
for (const auto& u : *it) {
auto user = UserFactory::create(u);
User user(u);
db[user.getUsername().getRawValue()] = user;
}
}
Expand Down
4 changes: 1 addition & 3 deletions cbsasl/password_database.h
@@ -1,4 +1,3 @@
/* -*- Mode: C++; tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- */
/*
* Copyright 2016-Present Couchbase, Inc.
*
Expand All @@ -10,11 +9,10 @@
*/
#pragma once

#include <cbsasl/user.h>
#include <nlohmann/json_fwd.hpp>

#include <string>
#include <unordered_map>
#include "user.h"

namespace cb::sasl::pwdb {

Expand Down
30 changes: 17 additions & 13 deletions cbsasl/password_database_test.cc
Expand Up @@ -9,11 +9,11 @@
*/

#include "password_database.h"
#include "user.h"

#include <cbcrypto/cbcrypto.h>
#include <cbsasl/pwdb.h>
#include <cbsasl/server.h>
#include <cbsasl/user.h>
#include <folly/portability/GTest.h>
#include <folly/portability/Stdlib.h>
#include <nlohmann/json.hpp>
Expand Down Expand Up @@ -101,6 +101,11 @@ class UserTest : public ::testing::Test {
public:
void SetUp() override {
root["n"] = "username";
root["uuid"] = "00000000-0000-0000-0000-000000000000";
root["limits"] = {{"ingress_mib_per_min", 10},
{"egress_mib_per_min", 20},
{"num_connections", 1},
{"num_ops_per_min", 1000}};
root["plain"] = Couchbase::Base64::encode("secret");

nlohmann::json sha1;
Expand Down Expand Up @@ -133,9 +138,13 @@ class UserTest : public ::testing::Test {

TEST_F(UserTest, TestNormalInit) {
using namespace cb::sasl;
pwdb::User u;
EXPECT_NO_THROW(u = pwdb::UserFactory::create(root));
pwdb::User u(root);
EXPECT_EQ("username", u.getUsername().getRawValue());
EXPECT_EQ(1, u.getLimits().num_connections);
EXPECT_EQ(1000, u.getLimits().num_ops_per_min);
EXPECT_EQ(10, u.getLimits().ingress_mib_per_min);
EXPECT_EQ(20, u.getLimits().egress_mib_per_min);
EXPECT_EQ(cb::uuid::uuid_t{}, u.getUuid());
EXPECT_NO_THROW(u.getPassword(Mechanism::SCRAM_SHA512));
EXPECT_NO_THROW(u.getPassword(Mechanism::SCRAM_SHA256));
EXPECT_NO_THROW(u.getPassword(Mechanism::SCRAM_SHA1));
Expand Down Expand Up @@ -184,8 +193,7 @@ TEST_F(UserTest, TestNoPlaintext) {
using namespace cb::sasl;

root.erase("plain");
pwdb::User u;
EXPECT_NO_THROW(u = pwdb::UserFactory::create(root));
pwdb::User u(root);
EXPECT_NO_THROW(u.getPassword(Mechanism::SCRAM_SHA512));
EXPECT_NO_THROW(u.getPassword(Mechanism::SCRAM_SHA256));
EXPECT_NO_THROW(u.getPassword(Mechanism::SCRAM_SHA1));
Expand All @@ -196,8 +204,7 @@ TEST_F(UserTest, TestNoSha512) {
using namespace cb::sasl;

root.erase("sha512");
pwdb::User u;
EXPECT_NO_THROW(u = pwdb::UserFactory::create(root));
pwdb::User u(root);
EXPECT_THROW(u.getPassword(Mechanism::SCRAM_SHA512), std::invalid_argument);
EXPECT_NO_THROW(u.getPassword(Mechanism::SCRAM_SHA256));
EXPECT_NO_THROW(u.getPassword(Mechanism::SCRAM_SHA1));
Expand All @@ -208,8 +215,7 @@ TEST_F(UserTest, TestNoSha256) {
using namespace cb::sasl;

root.erase("sha256");
pwdb::User u;
EXPECT_NO_THROW(u = pwdb::UserFactory::create(root));
pwdb::User u(root);
EXPECT_THROW(u.getPassword(Mechanism::SCRAM_SHA256), std::invalid_argument);
EXPECT_NO_THROW(u.getPassword(Mechanism::SCRAM_SHA512));
EXPECT_NO_THROW(u.getPassword(Mechanism::SCRAM_SHA1));
Expand All @@ -220,8 +226,7 @@ TEST_F(UserTest, TestNoSha1) {
using namespace cb::sasl;

root.erase("sha1");
pwdb::User u;
EXPECT_NO_THROW(u = pwdb::UserFactory::create(root));
pwdb::User u(root);
EXPECT_THROW(u.getPassword(Mechanism::SCRAM_SHA1), std::invalid_argument);
EXPECT_NO_THROW(u.getPassword(Mechanism::SCRAM_SHA512));
EXPECT_NO_THROW(u.getPassword(Mechanism::SCRAM_SHA256));
Expand All @@ -230,8 +235,7 @@ TEST_F(UserTest, TestNoSha1) {

TEST_F(UserTest, InvalidLabel) {
root["gssapi"] = "foo";
EXPECT_THROW(auto u = cb::sasl::pwdb::UserFactory::create(root),
std::runtime_error);
EXPECT_THROW(cb::sasl::pwdb::User u(root), std::runtime_error);
}

/**
Expand Down
4 changes: 1 addition & 3 deletions cbsasl/pwconv.cc
@@ -1,4 +1,3 @@
/* -*- Mode: C++; tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- */
/*
* Copyright 2016-Present Couchbase, Inc.
*
Expand All @@ -10,8 +9,7 @@
*/
#include <cbsasl/logging.h>
#include <cbsasl/pwdb.h>
#include "user.h"

#include <cbsasl/user.h>
#include <nlohmann/json.hpp>
#include <platform/dirutils.h>
#include <fstream>
Expand Down
2 changes: 1 addition & 1 deletion cbsasl/pwfile.h
Expand Up @@ -9,8 +9,8 @@
*/
#pragma once

#include <cbsasl/user.h>
#include <string>
#include "user.h"

/**
* Searches for a user entry for the specified user.
Expand Down
153 changes: 96 additions & 57 deletions cbsasl/user.cc
@@ -1,4 +1,3 @@
/* -*- Mode: C++; tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- */
/*
* Copyright 2016-Present Couchbase, Inc.
*
Expand All @@ -9,8 +8,7 @@
* the file licenses/APL2.txt.
*/

#include "user.h"

#include <cbsasl/user.h>
#include <gsl/gsl-lite.hpp>
#include <nlohmann/json.hpp>
#include <platform/base64.h>
Expand All @@ -23,6 +21,59 @@

namespace cb::sasl::pwdb {

static std::string getUsernameField(const nlohmann::json& json) {
if (!json.is_object()) {
throw std::runtime_error(
"cb::sasl::pwdb::getUsernameField(): provided json MUST be an "
"object");
}
auto n = json.find("n");
if (n == json.end()) {
throw std::runtime_error(
"cb::sasl::pwdb::getUsernameField(): missing mandatory label "
"'n'");
}
if (!n->is_string()) {
throw std::runtime_error(
"cb::sasl::pwdb::getUsernameField(): 'n' must be a string");
}
return n->get<std::string>();
}

User::User(const nlohmann::json& json)
: username(getUsernameField(json)), dummy(false) {
// getUsernameField validated that json is in fact an object.
// Iterate over the rest of the fields and pick out the values

for (auto it = json.begin(); it != json.end(); ++it) {
std::string label(it.key());
if (label == "n") {
// skip. we've already processed this
} else if (label == "sha512") {
User::PasswordMetaData pd(it.value());
password[Mechanism::SCRAM_SHA512] = pd;
} else if (label == "sha256") {
User::PasswordMetaData pd(it.value());
password[Mechanism::SCRAM_SHA256] = pd;
} else if (label == "sha1") {
User::PasswordMetaData pd(it.value());
password[Mechanism::SCRAM_SHA1] = pd;
} else if (label == "plain") {
User::PasswordMetaData pd(Couchbase::Base64::decode(it.value()));
password[Mechanism::PLAIN] = pd;
} else if (label == "uuid") {
uuid = cb::uuid::from_string(it.value().get<std::string>());
} else if (label == "limits") {
user::from_json(it.value(), limits);
} else {
throw std::runtime_error(
"cb::sasl::pwdb::User::User(): Invalid "
"label \"" +
label + "\" specified");
}
}
}

std::atomic<int> IterationCount(4096);

class ScamShaFallbackSalt {
Expand Down Expand Up @@ -138,55 +189,6 @@ User UserFactory::createDummy(const std::string& unm, const Mechanism& mech) {
return ret;
}

User UserFactory::create(const nlohmann::json& obj) {
if (obj == nullptr) {
throw std::runtime_error(
"cb::cbsasl::UserFactory::create: obj cannot be null");
}
if (!obj.is_object()) {
throw std::runtime_error(
"cb::cbsasl::UserFactory::create: Invalid object type");
}

auto n = obj.find("n");
if (n == obj.end()) {
throw std::runtime_error(
"cb::cbsasl::UserFactory::create: missing mandatory label 'n'");
}
if (!n->is_string()) {
throw std::runtime_error(
"cb::cbsasl::UserFactory::create: 'n' must be a string");
}

User ret{*n, false};

for (auto it = obj.begin(); it != obj.end(); ++it) {
std::string label(it.key());
if (label == "n") {
// skip. we've already processed this
} else if (label == "sha512") {
User::PasswordMetaData pd(it.value());
ret.password[Mechanism::SCRAM_SHA512] = pd;
} else if (label == "sha256") {
User::PasswordMetaData pd(it.value());
ret.password[Mechanism::SCRAM_SHA256] = pd;
} else if (label == "sha1") {
User::PasswordMetaData pd(it.value());
ret.password[Mechanism::SCRAM_SHA1] = pd;
} else if (label == "plain") {
User::PasswordMetaData pd(Couchbase::Base64::decode(it.value()));
ret.password[Mechanism::PLAIN] = pd;
} else {
throw std::runtime_error(
"cb::cbsasl::UserFactory::create: Invalid "
"label \"" +
label + "\" specified");
}
}

return ret;
}

void UserFactory::setDefaultHmacIterationCount(int count) {
IterationCount.store(count);
}
Expand All @@ -195,7 +197,7 @@ void UserFactory::setScramshaFallbackSalt(const std::string& salt) {
scramsha_fallback_salt.set(salt);
}

void User::generateSecrets(const Mechanism& mech, const std::string& passwd) {
void User::generateSecrets(Mechanism mech, std::string_view passwd) {
std::vector<uint8_t> salt;
std::string encodedSalt;
cb::crypto::Algorithm algorithm = cb::crypto::Algorithm::MD5;
Expand Down Expand Up @@ -364,14 +366,15 @@ nlohmann::json User::to_json() const {
"cb::cbsasl::User::toJSON(): Unsupported mech");
}
}
ret["uuid"] = ::to_string(uuid);
nlohmann::json json = limits;
if (!json.empty()) {
ret["limits"] = limits;
}

return ret;
}

std::string User::to_string() const {
return to_json().dump();
}

const User::PasswordMetaData& User::getPassword(const Mechanism& mech) const {
const auto iter = password.find(mech);

Expand All @@ -384,4 +387,40 @@ const User::PasswordMetaData& User::getPassword(const Mechanism& mech) const {
}
}

namespace user {
void to_json(nlohmann::json& json, const Limits& limits) {
json = {{"ingress_mib_per_min", limits.ingress_mib_per_min},
{"egress_mib_per_min", limits.egress_mib_per_min},
{"num_connections", limits.num_connections},
{"num_ops_per_min", limits.num_ops_per_min}};
}

void from_json(const nlohmann::json& json, Limits& limits) {
limits = {};
for (auto it = json.begin(); it != json.end(); ++it) {
const std::string label(it.key());
if (!it.value().is_number()) {
throw std::runtime_error(
"cb::sasl::pwdb::user::from_json: All limits must be "
"numeric values: " +
label);
}
if (label == "ingress_mib_per_min") {
limits.ingress_mib_per_min = it.value().get<uint64_t>();
} else if (label == "egress_mib_per_min") {
limits.egress_mib_per_min = it.value().get<uint64_t>();
} else if (label == "num_connections") {
limits.num_connections = it.value().get<uint64_t>();
} else if (label == "num_ops_per_min") {
limits.num_ops_per_min = it.value().get<uint64_t>();

} else {
throw std::runtime_error(
"cb::sasl::pwdb::user::from_json: Invalid "
"label \"" +
label + "\" specified");
}
}
}
} // namespace user
} // namespace cb::sasl::pwdb

0 comments on commit 27c9335

Please sign in to comment.