diff options
author | Spencer T Brody <spencer@10gen.com> | 2013-08-26 20:02:22 -0400 |
---|---|---|
committer | Spencer T Brody <spencer@10gen.com> | 2013-09-06 11:29:57 -0400 |
commit | 25785ee485220aa468fbc3eedfec1f05b36d502a (patch) | |
tree | 65bc1cba1e163291b5596280ded1868dd08562fc /src/mongo | |
parent | b9a1874e3e839aa130fe73d112470debb33e59b8 (diff) | |
download | mongo-25785ee485220aa468fbc3eedfec1f05b36d502a.tar.gz |
SERVER-6246 Change user management commands to use the new v2 style user documents
Diffstat (limited to 'src/mongo')
22 files changed, 1051 insertions, 362 deletions
diff --git a/src/mongo/SConscript b/src/mongo/SConscript index 09d0d37fc2b..e67512f31d8 100644 --- a/src/mongo/SConscript +++ b/src/mongo/SConscript @@ -17,6 +17,7 @@ Import("darwin windows solaris linux nix") env.SConscript(['base/SConscript', 'db/auth/SConscript', + 'db/commands/SConscript', 'db/exec/SConscript', 'db/fts/SConscript', 'db/ops/SConscript', @@ -356,6 +357,7 @@ env.StaticLibrary("coredb", [ "s/shardconnection.cpp", ], LIBDEPS=['db/auth/serverauth', + 'db/commands/usercommandsparser', 'db/common', 'server_parameters', 'geoparser', diff --git a/src/mongo/db/auth/authorization_manager.cpp b/src/mongo/db/auth/authorization_manager.cpp index 513be44b3f0..0158134251b 100644 --- a/src/mongo/db/auth/authorization_manager.cpp +++ b/src/mongo/db/auth/authorization_manager.cpp @@ -324,9 +324,9 @@ namespace { return _externalState->updatePrivilegeDocument(user, updateObj); } - Status AuthorizationManager::removePrivilegeDocuments(const std::string& dbname, - const BSONObj& query) const { - return _externalState->removePrivilegeDocuments(dbname, query); + Status AuthorizationManager::removePrivilegeDocuments(const BSONObj& query, + int* numRemoved) const { + return _externalState->removePrivilegeDocuments(query, numRemoved); } ActionSet AuthorizationManager::getAllUserActions() const { @@ -339,6 +339,54 @@ namespace { return allActions; } + bool AuthorizationManager::roleExists(const RoleName& role) const { + bool isAdminDB = role.getDB() == "admin"; + + if (role.getRole() == SYSTEM_ROLE_READ) { + return true; + } + else if (role.getRole() == SYSTEM_ROLE_READ_WRITE) { + return true; + } + else if (role.getRole() == SYSTEM_ROLE_USER_ADMIN) { + return true; + } + else if (role.getRole() == SYSTEM_ROLE_DB_ADMIN) { + return true; + } + else if (role.getRole() == AuthorizationManager::SYSTEM_ROLE_V0_READ) { + return true; + } + else if (role.getRole() == AuthorizationManager::SYSTEM_ROLE_V0_READ_WRITE) { + return true; + } + else if (isAdminDB && role.getRole() == SYSTEM_ROLE_READ_ANY_DB) { + return true; + } + else if (isAdminDB && role.getRole() == SYSTEM_ROLE_READ_WRITE_ANY_DB) { + return true; + } + else if (isAdminDB && role.getRole() == SYSTEM_ROLE_USER_ADMIN_ANY_DB) { + return true; + } + else if (isAdminDB && role.getRole() == SYSTEM_ROLE_DB_ADMIN_ANY_DB) { + return true; + } + else if (isAdminDB && role.getRole() == SYSTEM_ROLE_CLUSTER_ADMIN) { + return true; + } + else if (isAdminDB && role.getRole() == AuthorizationManager::SYSTEM_ROLE_V0_ADMIN_READ) { + return true; + } + else if (isAdminDB && + role.getRole() == AuthorizationManager::SYSTEM_ROLE_V0_ADMIN_READ_WRITE) { + return true; + } else { + // TODO(spencer): Check the role graph to see if this is a valid user-defined-role + return false; + } + + } /** * Adds to "outPrivileges" the privileges associated with having the named "role" on "dbname". * @@ -510,13 +558,42 @@ namespace { unordered_map<UserName, User*>::iterator it = _userCache.find(user->getName()); massert(17052, mongoutils::str::stream() << - "Invalidating cache for user " << user->getName().toString() << + "Invalidating cache for user " << user->getName().getFullName() << " failed as it is not present in the user cache", it != _userCache.end() && it->second == user); _userCache.erase(it); user->invalidate(); } + void AuthorizationManager::invalidateUserByName(const UserName& userName) { + boost::lock_guard<boost::mutex> lk(_lock); + + unordered_map<UserName, User*>::iterator it = _userCache.find(userName); + if (it == _userCache.end()) { + return; + } + + User* user = it->second; + _userCache.erase(it); + user->invalidate(); + } + + void AuthorizationManager::invalidateUsersFromDB(const std::string& dbname) { + boost::lock_guard<boost::mutex> lk(_lock); + + unordered_map<UserName, User*>::iterator it = _userCache.begin(); + while (it != _userCache.end()) { + User* user = it->second; + if (user->getName().getDB() == dbname) { + _userCache.erase(it++); + user->invalidate(); + } else { + ++it; + } + } + } + + void AuthorizationManager::addInternalUser(User* user) { boost::lock_guard<boost::mutex> lk(_lock); _userCache.insert(make_pair(user->getName(), user)); diff --git a/src/mongo/db/auth/authorization_manager.h b/src/mongo/db/auth/authorization_manager.h index dee227114db..f637639247c 100644 --- a/src/mongo/db/auth/authorization_manager.h +++ b/src/mongo/db/auth/authorization_manager.h @@ -112,7 +112,8 @@ namespace mongo { Status updatePrivilegeDocument(const UserName& user, const BSONObj& updateObj) const; // Removes users for the given database matching the given query. - Status removePrivilegeDocuments(const std::string& dbname, const BSONObj& query) const; + // Writes into *numRemoved the number of user documents that were modified. + Status removePrivilegeDocuments(const BSONObj& query, int* numRemoved) const; // Checks to see if "doc" is a valid privilege document, assuming it is stored in the // "system.users" collection of database "dbname". @@ -152,12 +153,27 @@ namespace mongo { void invalidateUser(User* user); /** + * Marks the given user as invalid and removes it from the user cache. + */ + void invalidateUserByName(const UserName& user); + + /** + * Invalidates all users who's source is "dbname" and removes them from the user cache. + */ + void invalidateUsersFromDB(const std::string& dbname); + + /** * Inserts the given user directly into the _userCache. Used to add the internalSecurity * user into the cache at process startup. */ void addInternalUser(User* user); /** + * Returns true if the role name given refers to a valid system or user defined role. + */ + bool roleExists(const RoleName& role) const; + + /** * Initializes the user cache with User objects for every v0 and v1 user document in the * system, by reading the system.users collection of every database. If this function * returns a non-ok Status, the _userCache should be considered corrupt and must be diff --git a/src/mongo/db/auth/authz_manager_external_state.h b/src/mongo/db/auth/authz_manager_external_state.h index 89ecbc3603e..97b0aeb5797 100644 --- a/src/mongo/db/auth/authz_manager_external_state.h +++ b/src/mongo/db/auth/authz_manager_external_state.h @@ -62,9 +62,8 @@ namespace mongo { const BSONObj& updateObj) = 0; // Removes users for the given database matching the given query. - // TODO(spencer): remove dbname argument once users are only written into the admin db - virtual Status removePrivilegeDocuments(const std::string& dbname, - const BSONObj& query) = 0; + // Writes into *numRemoved the number of user documents that were modified. + virtual Status removePrivilegeDocuments(const BSONObj& query, int* numRemoved) = 0; /** * Puts into the *dbnames vector the name of every database in the cluster. diff --git a/src/mongo/db/auth/authz_manager_external_state_d.cpp b/src/mongo/db/auth/authz_manager_external_state_d.cpp index fb7a999f268..5104ecef786 100644 --- a/src/mongo/db/auth/authz_manager_external_state_d.cpp +++ b/src/mongo/db/auth/authz_manager_external_state_d.cpp @@ -17,6 +17,7 @@ #include "mongo/db/auth/authz_manager_external_state_d.h" #include "mongo/base/status.h" +#include "mongo/db/auth/authorization_manager.h" #include "mongo/db/auth/user_name.h" #include "mongo/db/client.h" #include "mongo/db/d_concurrency.h" @@ -38,7 +39,7 @@ namespace { Status AuthzManagerExternalStateMongod::insertPrivilegeDocument(const string& dbname, const BSONObj& userObj) { try { - string userNS = dbname + ".system.users"; + const std::string userNS = "admin.system.users"; DBDirectClient client; { Client::GodScope gs; @@ -57,9 +58,11 @@ namespace { return Status::OK(); } if (res.hasField("code") && res["code"].Int() == ASSERT_ID_DUPKEY) { + std::string name = userObj[AuthorizationManager::USER_NAME_FIELD_NAME].String(); + std::string source = userObj[AuthorizationManager::USER_SOURCE_FIELD_NAME].String(); return Status(ErrorCodes::DuplicateKey, - mongoutils::str::stream() << "User \"" << userObj["user"].String() << - "\" already exists on database \"" << dbname << "\""); + mongoutils::str::stream() << "User \"" << name << "@" << source << + "\" already exists"); } return Status(ErrorCodes::UserModificationFailed, errstr); } catch (const DBException& e) { @@ -70,7 +73,7 @@ namespace { Status AuthzManagerExternalStateMongod::updatePrivilegeDocument( const UserName& user, const BSONObj& updateObj) { try { - string userNS = mongoutils::str::stream() << user.getDB() << ".system.users"; + const std::string userNS = "admin.system.users"; DBDirectClient client; { Client::GodScope gs; @@ -80,7 +83,8 @@ namespace { Lock::GlobalWrite w; // Client::WriteContext ctx(userNS); client.update(userNS, - QUERY("user" << user.getUser() << "userSource" << BSONNULL), + QUERY(AuthorizationManager::USER_NAME_FIELD_NAME << user.getUser() << + AuthorizationManager::USER_SOURCE_FIELD_NAME << user.getDB()), updateObj); } @@ -105,10 +109,10 @@ namespace { } } - Status AuthzManagerExternalStateMongod::removePrivilegeDocuments(const string& dbname, - const BSONObj& query) { + Status AuthzManagerExternalStateMongod::removePrivilegeDocuments(const BSONObj& query, + int* numRemoved) { try { - string userNS = dbname + ".system.users"; + const std::string userNS = "admin.system.users"; DBDirectClient client; { Client::GodScope gs; @@ -127,12 +131,7 @@ namespace { return Status(ErrorCodes::UserModificationFailed, errstr); } - int numUpdated = res["n"].numberInt(); - if (numUpdated == 0) { - return Status(ErrorCodes::UserNotFound, - mongoutils::str::stream() << "No users found on database \"" << dbname - << "\" matching query: " << query.toString()); - } + *numRemoved = res["n"].numberInt(); return Status::OK(); } catch (const DBException& e) { return e.toStatus(); diff --git a/src/mongo/db/auth/authz_manager_external_state_d.h b/src/mongo/db/auth/authz_manager_external_state_d.h index 1d0b0eb9d8e..e706fd40d2e 100644 --- a/src/mongo/db/auth/authz_manager_external_state_d.h +++ b/src/mongo/db/auth/authz_manager_external_state_d.h @@ -41,8 +41,7 @@ namespace mongo { virtual Status updatePrivilegeDocument(const UserName& user, const BSONObj& updateObj); - virtual Status removePrivilegeDocuments(const std::string& dbname, - const BSONObj& query); + virtual Status removePrivilegeDocuments(const BSONObj& query, int* numRemoved); virtual Status getAllDatabaseNames(std::vector<std::string>* dbnames); diff --git a/src/mongo/db/auth/authz_manager_external_state_mock.cpp b/src/mongo/db/auth/authz_manager_external_state_mock.cpp index 0cdd2e20462..82afca81614 100644 --- a/src/mongo/db/auth/authz_manager_external_state_mock.cpp +++ b/src/mongo/db/auth/authz_manager_external_state_mock.cpp @@ -36,8 +36,8 @@ namespace mongo { return Status(ErrorCodes::InternalError, "Not implemented in mock."); } - Status AuthzManagerExternalStateMock::removePrivilegeDocuments(const std::string& dbname, - const BSONObj& query) { + Status AuthzManagerExternalStateMock::removePrivilegeDocuments(const BSONObj& query, + int* numRemoved) { return Status(ErrorCodes::InternalError, "Not implemented in mock."); } diff --git a/src/mongo/db/auth/authz_manager_external_state_mock.h b/src/mongo/db/auth/authz_manager_external_state_mock.h index e835966929e..4c045be3b50 100644 --- a/src/mongo/db/auth/authz_manager_external_state_mock.h +++ b/src/mongo/db/auth/authz_manager_external_state_mock.h @@ -44,8 +44,8 @@ namespace mongo { virtual Status updatePrivilegeDocument(const UserName& user, const BSONObj& updateObj); - virtual Status removePrivilegeDocuments(const std::string& dbname, - const BSONObj& query); + // no-op for the mock + virtual Status removePrivilegeDocuments(const BSONObj& query, int* numRemoved); void clearPrivilegeDocuments(); diff --git a/src/mongo/db/auth/authz_manager_external_state_s.cpp b/src/mongo/db/auth/authz_manager_external_state_s.cpp index ed6fd9e9828..d23edfac597 100644 --- a/src/mongo/db/auth/authz_manager_external_state_s.cpp +++ b/src/mongo/db/auth/authz_manager_external_state_s.cpp @@ -19,6 +19,7 @@ #include <string> #include "mongo/client/dbclientinterface.h" +#include "mongo/db/auth/authorization_manager.h" #include "mongo/db/auth/user_name.h" #include "mongo/db/jsobj.h" #include "mongo/s/type_database.h" @@ -69,7 +70,7 @@ namespace { Status AuthzManagerExternalStateMongos::insertPrivilegeDocument(const string& dbname, const BSONObj& userObj) { try { - string userNS = dbname + ".system.users"; + const std::string userNS = "admin.system.users"; scoped_ptr<ScopedDbConnection> conn(getConnectionForUsersCollection(userNS)); conn->get()->insert(userNS, userObj); @@ -83,9 +84,11 @@ namespace { return Status::OK(); } if (res.hasField("code") && res["code"].Int() == ASSERT_ID_DUPKEY) { + std::string name = userObj[AuthorizationManager::USER_NAME_FIELD_NAME].String(); + std::string source = userObj[AuthorizationManager::USER_SOURCE_FIELD_NAME].String(); return Status(ErrorCodes::DuplicateKey, - mongoutils::str::stream() << "User \"" << userObj["user"].String() << - "\" already exists on database \"" << dbname << "\""); + mongoutils::str::stream() << "User \"" << name << "@" << source << + "\" already exists"); } return Status(ErrorCodes::UserModificationFailed, errstr); } catch (const DBException& e) { @@ -96,12 +99,14 @@ namespace { Status AuthzManagerExternalStateMongos::updatePrivilegeDocument( const UserName& user, const BSONObj& updateObj) { try { - string userNS = mongoutils::str::stream() << user.getDB() << ".system.users"; + const std::string userNS = "admin.system.users"; scoped_ptr<ScopedDbConnection> conn(getConnectionForUsersCollection(userNS)); - conn->get()->update(userNS, - QUERY("user" << user.getUser() << "userSource" << BSONNULL), - updateObj); + conn->get()->update( + userNS, + QUERY(AuthorizationManager::USER_NAME_FIELD_NAME << user.getUser() << + AuthorizationManager::USER_SOURCE_FIELD_NAME << user.getDB()), + updateObj); // 30 second timeout for w:majority BSONObj res = conn->get()->getLastErrorDetailed(false, false, -1, 30*1000); @@ -126,10 +131,10 @@ namespace { } } - Status AuthzManagerExternalStateMongos::removePrivilegeDocuments(const string& dbname, - const BSONObj& query) { + Status AuthzManagerExternalStateMongos::removePrivilegeDocuments(const BSONObj& query, + int* numRemoved) { try { - string userNS = dbname + ".system.users"; + string userNS = "admin.system.users"; scoped_ptr<ScopedDbConnection> conn(getConnectionForUsersCollection(userNS)); conn->get()->remove(userNS, query); @@ -143,12 +148,7 @@ namespace { return Status(ErrorCodes::UserModificationFailed, err); } - int numUpdated = res["n"].numberInt(); - if (numUpdated == 0) { - return Status(ErrorCodes::UserNotFound, - mongoutils::str::stream() << "No users found on database \"" << dbname - << "\" matching query: " << query.toString()); - } + *numRemoved = res["n"].numberInt(); return Status::OK(); } catch (const DBException& e) { return e.toStatus(); diff --git a/src/mongo/db/auth/authz_manager_external_state_s.h b/src/mongo/db/auth/authz_manager_external_state_s.h index 12cbfb667f7..ab711ef3123 100644 --- a/src/mongo/db/auth/authz_manager_external_state_s.h +++ b/src/mongo/db/auth/authz_manager_external_state_s.h @@ -41,8 +41,7 @@ namespace mongo { virtual Status updatePrivilegeDocument(const UserName& user, const BSONObj& updateObj); - virtual Status removePrivilegeDocuments(const std::string& dbname, - const BSONObj& query); + virtual Status removePrivilegeDocuments(const BSONObj& query, int* numRemoved); virtual Status getAllDatabaseNames(std::vector<std::string>* dbnames); diff --git a/src/mongo/db/auth/privilege_document_parser.cpp b/src/mongo/db/auth/privilege_document_parser.cpp index b9ebb9b3386..c30f2e0b89c 100644 --- a/src/mongo/db/auth/privilege_document_parser.cpp +++ b/src/mongo/db/auth/privilege_document_parser.cpp @@ -452,6 +452,41 @@ namespace { return Status::OK(); } + Status V2PrivilegeDocumentParser::checkValidRoleObject( + const BSONObj& roleObject) const { + BSONElement roleNameElement = roleObject[ROLE_NAME_FIELD_NAME]; + BSONElement roleSourceElement = roleObject[ROLE_SOURCE_FIELD_NAME]; + BSONElement canDelegateElement = roleObject[ROLE_CAN_DELEGATE_FIELD_NAME]; + BSONElement hasRoleElement = roleObject[ROLE_HAS_ROLE_FIELD_NAME]; + + if (roleNameElement.type() != String || + makeStringDataFromBSONElement(roleNameElement).empty()) { + return Status(ErrorCodes::UnsupportedFormat, + "Role names must be non-empty strings"); + } + if (roleSourceElement.type() != String || + makeStringDataFromBSONElement(roleSourceElement).empty()) { + return Status(ErrorCodes::UnsupportedFormat, + "Role source must be non-empty strings"); + } + if (canDelegateElement.type() != Bool) { + return Status(ErrorCodes::UnsupportedFormat, + "Entries in 'roles' array need a 'canDelegate' boolean field"); + } + if (hasRoleElement.type() != Bool) { + return Status(ErrorCodes::UnsupportedFormat, + "Entries in 'roles' array need a 'hasRole' boolean field"); + } + + if (!hasRoleElement.Bool() && !canDelegateElement.Bool()) { + return Status(ErrorCodes::UnsupportedFormat, + "At least one of 'canDelegate' and 'hasRole' must be true for " + "every role in the 'roles' array"); + } + + return Status::OK(); + } + Status V2PrivilegeDocumentParser::initializeUserRolesFromPrivilegeDocument( User* user, const BSONObj& privDoc, const StringData&) const { @@ -469,38 +504,21 @@ namespace { } BSONObj roleObject = (*it).Obj(); + Status status = checkValidRoleObject(roleObject); + if (!status.isOK()) { + return status; + } + BSONElement roleNameElement = roleObject[ROLE_NAME_FIELD_NAME]; BSONElement roleSourceElement = roleObject[ROLE_SOURCE_FIELD_NAME]; BSONElement canDelegateElement = roleObject[ROLE_CAN_DELEGATE_FIELD_NAME]; BSONElement hasRoleElement = roleObject[ROLE_HAS_ROLE_FIELD_NAME]; - if (roleNameElement.type() != String || - makeStringDataFromBSONElement(roleNameElement).empty()) { - return Status(ErrorCodes::UnsupportedFormat, - "Role names must be non-empty strings"); - } - if (roleSourceElement.type() != String || - makeStringDataFromBSONElement(roleSourceElement).empty()) { - return Status(ErrorCodes::UnsupportedFormat, - "Role source must be non-empty strings"); - } - if (canDelegateElement.type() != Bool) { - return Status(ErrorCodes::UnsupportedFormat, - "Entries in 'roles' array need a 'canDelegate' boolean field"); - } - if (hasRoleElement.type() != Bool) { - return Status(ErrorCodes::UnsupportedFormat, - "Entries in 'roles' array need a 'hasRole' boolean field"); - } - if (hasRoleElement.Bool()) { user->addRole(RoleName(roleNameElement.String(), roleSourceElement.String())); - } else if (canDelegateElement.Bool()) { + } + if (canDelegateElement.Bool()) { // TODO(spencer): record the fact that this user can delegate this role - } else { - return Status(ErrorCodes::UnsupportedFormat, - "At least one of 'canDelegate' and 'hasRole' must be true for " - "every role in the 'roles' array"); } } return Status::OK(); diff --git a/src/mongo/db/auth/privilege_document_parser.h b/src/mongo/db/auth/privilege_document_parser.h index 19aa0d12ac5..ee24c79267b 100644 --- a/src/mongo/db/auth/privilege_document_parser.h +++ b/src/mongo/db/auth/privilege_document_parser.h @@ -90,6 +90,11 @@ namespace mongo { virtual Status checkValidPrivilegeDocument(const StringData& dbname, const BSONObj& doc) const; + /** + * Returns Status::OK() iff the given BSONObj describes a valid element from a roles array. + */ + Status checkValidRoleObject(const BSONObj& roleObject) const; + virtual std::string extractUserNameFromPrivilegeDocument(const BSONObj& doc) const; virtual Status initializeUserCredentialsFromPrivilegeDocument(User* user, diff --git a/src/mongo/db/auth/role_name.cpp b/src/mongo/db/auth/role_name.cpp index 8c66232abb7..28bf342bd18 100644 --- a/src/mongo/db/auth/role_name.cpp +++ b/src/mongo/db/auth/role_name.cpp @@ -60,4 +60,8 @@ namespace mongo { return new RoleNameSetIterator(_begin, _end); } + std::ostream& operator<<(std::ostream& os, const RoleName& name) { + return os << name.getFullName(); + } + } // namespace mongo diff --git a/src/mongo/db/auth/role_name.h b/src/mongo/db/auth/role_name.h index 130f7a98e70..9b0fc796631 100644 --- a/src/mongo/db/auth/role_name.h +++ b/src/mongo/db/auth/role_name.h @@ -16,6 +16,7 @@ #pragma once +#include <iosfwd> #include <string> #include <boost/scoped_ptr.hpp> @@ -77,6 +78,8 @@ namespace mongo { return lhs.getFullName() < rhs.getFullName(); } + std::ostream& operator<<(std::ostream& os, const RoleName& name); + /** * Iterator over an unspecified container of RoleName objects. diff --git a/src/mongo/db/auth/user_name.cpp b/src/mongo/db/auth/user_name.cpp index 59e7984e1a8..db3d25a69ec 100644 --- a/src/mongo/db/auth/user_name.cpp +++ b/src/mongo/db/auth/user_name.cpp @@ -35,4 +35,8 @@ namespace mongo { _splitPoint = user.size(); } + std::ostream& operator<<(std::ostream& os, const UserName& name) { + return os << name.getFullName(); + } + } // namespace mongo diff --git a/src/mongo/db/auth/user_name.h b/src/mongo/db/auth/user_name.h index 6efa7b695ed..acee87f8ada 100644 --- a/src/mongo/db/auth/user_name.h +++ b/src/mongo/db/auth/user_name.h @@ -15,6 +15,7 @@ #pragma once +#include <iosfwd> #include <string> #include "mongo/base/string_data.h" @@ -68,4 +69,6 @@ namespace mongo { return lhs.getFullName() < rhs.getFullName(); } + std::ostream& operator<<(std::ostream& os, const UserName& name); + } // namespace mongo diff --git a/src/mongo/db/auth/user_set_test.cpp b/src/mongo/db/auth/user_set_test.cpp index 3a2141855c6..fb30d00bee7 100644 --- a/src/mongo/db/auth/user_set_test.cpp +++ b/src/mongo/db/auth/user_set_test.cpp @@ -25,11 +25,6 @@ #define ASSERT_NULL(EXPR) ASSERT_FALSE((EXPR)) namespace mongo { - - static inline std::ostream& operator<<(std::ostream& os, const UserName& uname) { - return os << uname.toString(); - } - namespace { TEST(UserSetTest, BasicTest) { diff --git a/src/mongo/db/commands/SConscript b/src/mongo/db/commands/SConscript new file mode 100644 index 00000000000..0e01774275c --- /dev/null +++ b/src/mongo/db/commands/SConscript @@ -0,0 +1,14 @@ +# -*- mode: python -*- + +Import("env") + +env.StaticLibrary('usercommandsparser', + ['user_management_commands_parser.cpp'], + LIBDEPS=['$BUILD_DIR/mongo/db/auth/authcore', + '$BUILD_DIR/mongo/bson']) + +env.CppUnitTest('user_management_commands_parser_test', 'user_management_commands_parser_test.cpp', + LIBDEPS=['usercommandsparser', + '$BUILD_DIR/mongo/mongocommon', + '$BUILD_DIR/mongo/db/auth/authmocks'], + NO_CRUTCH=True) diff --git a/src/mongo/db/commands/user_management_commands.cpp b/src/mongo/db/commands/user_management_commands.cpp index 2a40a3a4e71..48409a43bf5 100644 --- a/src/mongo/db/commands/user_management_commands.cpp +++ b/src/mongo/db/commands/user_management_commands.cpp @@ -27,7 +27,9 @@ #include "mongo/db/auth/authorization_manager.h" #include "mongo/db/auth/authorization_manager_global.h" #include "mongo/db/auth/privilege.h" +#include "mongo/db/auth/user.h" #include "mongo/db/commands.h" +#include "mongo/db/commands/user_management_commands_parser.h" #include "mongo/db/jsobj.h" #include "mongo/platform/unordered_set.h" #include "mongo/util/mongoutils/str.h" @@ -83,89 +85,28 @@ namespace mongo { out->push_back(Privilege(dbname, actions)); } - struct CreateUserArgs { - std::string userName; - std::string clearTextPassword; - std::string userSource; // TODO(spencer): remove this once we're using v2 user format - bool readOnly; // TODO(spencer): remove this once we're using the new v2 user format - BSONObj extraData; // Owned by the owner of the command object used to call createUser - BSONArray roles; // Owned by the owner of the command object used to call createUser - // Owned by the owner of the command object used to call createUser - // TODO(spencer): remove otherDBRoles once we're using the new v2 user format - BSONObj otherDBRoles; - bool hasPassword; - bool hasUserSource; - bool hasReadOnly; - bool hasExtraData; - bool hasRoles; - bool hasOtherDBRoles; - CreateUserArgs() : readOnly(false), hasPassword(false), hasUserSource(false), - hasReadOnly(false), hasExtraData(false), hasRoles(false), - hasOtherDBRoles(false) {} - }; - - // TODO: The bulk of the implementation of this will need to change once we're using the - // new v2 authorization storage format. bool run(const string& dbname, BSONObj& cmdObj, int options, string& errmsg, BSONObjBuilder& result, bool fromRepl) { - CreateUserArgs args; - Status status = _parseAndValidateInput(dbname, cmdObj, &args); - if (!status.isOK()) { - addStatus(status, result); - return false; - } - - std::string password = DBClientWithCommands::createPasswordDigest( - args.userName, args.clearTextPassword); - - - BSONObjBuilder userObjBuilder; - userObjBuilder.append("_id", OID::gen()); - userObjBuilder.append("user", args.userName); - if (args.hasPassword) { - userObjBuilder.append("pwd", password); - } - - if (args.hasUserSource) { - userObjBuilder.append("userSource", args.userSource); - } - - if (args.hasReadOnly) { - userObjBuilder.append("readOnly", args.readOnly); - } - - if (args.hasExtraData) { - userObjBuilder.append("extraData", args.extraData); - } - - if (args.hasRoles) { - userObjBuilder.append("roles", args.roles); - } - - if (args.hasOtherDBRoles) { - userObjBuilder.append("otherDBRoles", args.otherDBRoles); - } - AuthorizationManager* authzManager = getGlobalAuthorizationManager(); - status = authzManager->insertPrivilegeDocument(dbname, userObjBuilder.obj()); + BSONObj userObj; + Status status = auth::parseAndValidateCreateUserCommand(cmdObj, + dbname, + authzManager, + &userObj); if (!status.isOK()) { addStatus(status, result); return false; } - // Rebuild full user cache on every user modification. - // TODO(spencer): Remove this once we update user cache on-demand for each user - // modification. - status = authzManager->initializeAllV1UserData(); + status = authzManager->insertPrivilegeDocument(dbname, userObj); if (!status.isOK()) { addStatus(status, result); return false; } - return true; } @@ -173,122 +114,74 @@ namespace mongo { redactPasswordData(cmdObj->root()); } - private: - - Status _parseAndValidateInput(const std::string& dbname, - const BSONObj& cmdObj, - CreateUserArgs* parsedArgs) const { - unordered_set<std::string> validFieldNames; - validFieldNames.insert("createUser"); - validFieldNames.insert("user"); - validFieldNames.insert("pwd"); - validFieldNames.insert("userSource"); - validFieldNames.insert("roles"); - validFieldNames.insert("readOnly"); - validFieldNames.insert("otherDBRoles"); - - - // Iterate through all fields in command object and make sure there are no unexpected - // ones. - for (BSONObjIterator iter(cmdObj); iter.more(); iter.next()) { - StringData fieldName = (*iter).fieldNameStringData(); - if (!validFieldNames.count(fieldName.toString())) { - return Status(ErrorCodes::BadValue, - mongoutils::str::stream() << "\"" << fieldName << "\" is not " - "a valid argument to createUser"); - } - } + } cmdCreateUser; - Status status = bsonExtractStringField(cmdObj, "user", &parsedArgs->userName); - if (!status.isOK()) { - return status; - } + class CmdUpdateUser : public Command { + public: - if (cmdObj.hasField("pwd")) { - parsedArgs->hasPassword = true; - status = bsonExtractStringField(cmdObj, "pwd", &parsedArgs->clearTextPassword); - if (!status.isOK()) { - return status; - } - } + virtual bool logTheOp() { + return false; + } + virtual bool slaveOk() const { + return false; + } - if (cmdObj.hasField("userSource")) { - parsedArgs->hasUserSource = true; - status = bsonExtractStringField(cmdObj, "userSource", &parsedArgs->userSource); - if (!status.isOK()) { - return status; - } - } + virtual LockType locktype() const { + return NONE; + } - if (cmdObj.hasField("readOnly")) { - parsedArgs->hasReadOnly = true; - status = bsonExtractBooleanField(cmdObj, "readOnly", &parsedArgs->readOnly); - if (!status.isOK()) { - return status; - } - } + CmdUpdateUser() : Command("updateUser") {} - if (cmdObj.hasField("extraData")) { - parsedArgs->hasExtraData = true; - BSONElement element; - status = bsonExtractTypedField(cmdObj, "extraData", Object, &element); - if (!status.isOK()) { - return status; - } - parsedArgs->extraData = element.Obj(); - } + virtual void help(stringstream& ss) const { + ss << "Used to update a user, for example to change its password" << endl; + } - if (cmdObj.hasField("roles")) { - parsedArgs->hasRoles = true; - BSONElement element; - status = bsonExtractTypedField(cmdObj, "roles", Array, &element); - if (!status.isOK()) { - return status; - } - parsedArgs->roles = BSONArray(element.Obj()); - } + virtual void addRequiredPrivileges(const std::string& dbname, + const BSONObj& cmdObj, + std::vector<Privilege>* out) { + // TODO: update this with the new rules around user creation in 2.6. + ActionSet actions; + actions.addAction(ActionType::userAdmin); + out->push_back(Privilege(dbname, actions)); + } - if (cmdObj.hasField("otherDBRoles")) { - parsedArgs->hasOtherDBRoles = true; - BSONElement element; - status = bsonExtractTypedField(cmdObj, "otherDBRoles", Object, &element); - if (!status.isOK()) { - return status; - } - parsedArgs->otherDBRoles = element.Obj(); + bool run(const string& dbname, + BSONObj& cmdObj, + int options, + string& errmsg, + BSONObjBuilder& result, + bool fromRepl) { + AuthorizationManager* authzManager = getGlobalAuthorizationManager(); + BSONObj updateObj; + UserName userName; + Status status = auth::parseAndValidateUpdateUserCommand(cmdObj, + dbname, + authzManager, + &updateObj, + &userName); + if (!status.isOK()) { + addStatus(status, result); + return false; } - if (parsedArgs->hasPassword && parsedArgs->hasUserSource) { - return Status(ErrorCodes::BadValue, - "User objects can't have both 'pwd' and 'userSource'"); - } - if (!parsedArgs->hasPassword && !parsedArgs->hasUserSource) { - return Status(ErrorCodes::BadValue, - "User objects must have one of 'pwd' and 'userSource'"); - } - if (parsedArgs->hasRoles && parsedArgs->hasReadOnly) { - return Status(ErrorCodes::BadValue, - "User objects can't have both 'roles' and 'readOnly'"); + status = authzManager->updatePrivilegeDocument(userName, updateObj); + if (!status.isOK()) { + addStatus(status, result); + return false; } - // Prevent creating a __system user on the local database, and also prevent creating - // privilege documents in other datbases for the __system@local user. - // TODO(spencer): The second part will go away once we use the new V2 user doc format - // as it doesn't have the same userSource notion. - if (parsedArgs->userName == internalSecurity.user->getName().getUser() && - ((!parsedArgs->hasUserSource && dbname == "local") || - parsedArgs->userSource == "local")) { - return Status(ErrorCodes::BadValue, - "Cannot create user document for the internal user"); - } + authzManager->invalidateUserByName(userName); + return true; + } - return Status::OK(); + virtual void redactForLogging(mutablebson::Document* cmdObj) { + redactPasswordData(cmdObj->root()); } - } cmdCreateUser; + } cmdUpdateUser; - class CmdUpdateUser : public Command { + class CmdRemoveUser : public Command { public: virtual bool logTheOp() { @@ -303,10 +196,10 @@ namespace mongo { return NONE; } - CmdUpdateUser() : Command("updateUser") {} + CmdRemoveUser() : Command("removeUser") {} virtual void help(stringstream& ss) const { - ss << "Used to update a user, for example to change its password" << endl; + ss << "Removes a single user." << endl; } virtual void addRequiredPrivileges(const std::string& dbname, @@ -318,119 +211,46 @@ namespace mongo { out->push_back(Privilege(dbname, actions)); } - struct UpdateUserArgs { - std::string userName; - std::string clearTextPassword; - BSONObj extraData; // Owned by the owner of the command object given to updateUser - bool hasPassword; - bool hasExtraData; - UpdateUserArgs() : hasPassword(false), hasExtraData(false) {} - }; - bool run(const string& dbname, BSONObj& cmdObj, int options, string& errmsg, BSONObjBuilder& result, bool fromRepl) { - UpdateUserArgs args; - Status status = _parseAndValidateInput(cmdObj, &args); + std::string user; + + Status status = bsonExtractStringField(cmdObj, "removeUser", &user); if (!status.isOK()) { addStatus(status, result); return false; } - // TODO: This update will have to change once we're using the new v2 user - // storage format. - BSONObjBuilder setBuilder; - if (args.hasPassword) { - std::string password = DBClientWithCommands::createPasswordDigest( - args.userName, args.clearTextPassword); - setBuilder.append("pwd", password); - } - if (args.hasExtraData) { - setBuilder.append("extraData", args.extraData); - } - BSONObj updateObj = BSON("$set" << setBuilder.obj()); - + int numUpdated; AuthorizationManager* authzManager = getGlobalAuthorizationManager(); - status = authzManager->updatePrivilegeDocument(UserName(args.userName, dbname), - updateObj); - + status = authzManager->removePrivilegeDocuments( + BSON(AuthorizationManager::USER_NAME_FIELD_NAME << user << + AuthorizationManager::USER_SOURCE_FIELD_NAME << dbname), + &numUpdated); if (!status.isOK()) { addStatus(status, result); return false; } - // Rebuild full user cache on every user modification. - // TODO(spencer): Remove this once we update user cache on-demand for each user - // modification. - status = authzManager->initializeAllV1UserData(); - if (!status.isOK()) { - addStatus(status, result); + if (numUpdated == 0) { + addStatus(Status(ErrorCodes::UserNotFound, + mongoutils::str::stream() << "User '" << user << "@" << + dbname << "' not found"), + result); return false; } + authzManager->invalidateUserByName(UserName(user, dbname)); return true; } - virtual void redactForLogging(mutablebson::Document* cmdObj) { - redactPasswordData(cmdObj->root()); - } - - private: - - Status _parseAndValidateInput(BSONObj cmdObj, UpdateUserArgs* parsedArgs) const { - unordered_set<std::string> validFieldNames; - validFieldNames.insert("updateUser"); - validFieldNames.insert("user"); - validFieldNames.insert("pwd"); - validFieldNames.insert("extraData"); - - // Iterate through all fields in command object and make sure there are no - // unexpected ones. - for (BSONObjIterator iter(cmdObj); iter.more(); iter.next()) { - StringData fieldName = (*iter).fieldNameStringData(); - if (!validFieldNames.count(fieldName.toString())) { - return Status(ErrorCodes::BadValue, - mongoutils::str::stream() << "\"" << fieldName << "\" is not " - "a valid argument to updateUser"); - } - } - - Status status = bsonExtractStringField(cmdObj, "user", &parsedArgs->userName); - if (!status.isOK()) { - return status; - } - - if (cmdObj.hasField("pwd")) { - parsedArgs->hasPassword = true; - status = bsonExtractStringField(cmdObj, "pwd", &parsedArgs->clearTextPassword); - if (!status.isOK()) { - return status; - } - } - - if (cmdObj.hasField("extraData")) { - parsedArgs->hasExtraData = true; - BSONElement element; - status = bsonExtractTypedField(cmdObj, "extraData", Object, &element); - if (!status.isOK()) { - return status; - } - parsedArgs->extraData = element.Obj(); - } - - - if (!parsedArgs->hasPassword && !parsedArgs->hasExtraData) { - return Status(ErrorCodes::BadValue, - "Must specify at least one of 'pwd' and 'extraData'"); - } - return Status::OK(); - } - } cmdUpdateUser; + } cmdRemoveUser; - class CmdRemoveUsers : public Command { + class CmdRemoveUsersFromDatabase : public Command { public: virtual bool logTheOp() { @@ -445,11 +265,10 @@ namespace mongo { return NONE; } - CmdRemoveUsers() : Command("removeUsers") {} + CmdRemoveUsersFromDatabase() : Command("removeUsersFromDatabase") {} virtual void help(stringstream& ss) const { - ss << "By default, removes all users for this database. If given a \"user\"" - " argument, removes only that user."<< endl; + ss << "Removes all users for a single database." << endl; } virtual void addRequiredPrivileges(const std::string& dbname, @@ -461,46 +280,27 @@ namespace mongo { out->push_back(Privilege(dbname, actions)); } - // TODO: The bulk of the implementation of this will need to change once we're using the - // new v2 authorization storage format. bool run(const string& dbname, BSONObj& cmdObj, int options, string& errmsg, BSONObjBuilder& result, bool fromRepl) { - std::string user; - - if (cmdObj.hasField("user")) { - Status status = bsonExtractStringField(cmdObj, "user", &user); - if (!status.isOK()) { - addStatus(status, result); - return false; - } - } - - BSONObj query; - if (!user.empty()) { - query = BSON("user" << user); - } + int numRemoved; AuthorizationManager* authzManager = getGlobalAuthorizationManager(); - Status status = authzManager->removePrivilegeDocuments(dbname, query); + Status status = authzManager->removePrivilegeDocuments( + BSON(AuthorizationManager::USER_SOURCE_FIELD_NAME << dbname), + &numRemoved); if (!status.isOK()) { addStatus(status, result); return false; } - // Rebuild full user cache on every user modification. - // TODO(spencer): Remove this once we update user cache on-demand for each user - // modification. - status = authzManager->initializeAllV1UserData(); - if (!status.isOK()) { - addStatus(status, result); - return false; - } + result.append("n", numRemoved); + authzManager->invalidateUsersFromDB(dbname); return true; } - } cmdRemoveUser; + } cmdRemoveUsersFromDatabase; } diff --git a/src/mongo/db/commands/user_management_commands_parser.cpp b/src/mongo/db/commands/user_management_commands_parser.cpp new file mode 100644 index 00000000000..81a223a3a4e --- /dev/null +++ b/src/mongo/db/commands/user_management_commands_parser.cpp @@ -0,0 +1,285 @@ +/** +* Copyright (C) 2013 10gen Inc. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License, version 3, +* as published by the Free Software Foundation. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "mongo/db/commands/user_management_commands_parser.h" + +#include <string> + +#include "mongo/base/status.h" +#include "mongo/bson/util/bson_extract.h" +#include "mongo/client/auth_helpers.h" +#include "mongo/db/auth/authorization_manager.h" +#include "mongo/db/auth/privilege_document_parser.h" +#include "mongo/db/auth/user_name.h" +#include "mongo/db/jsobj.h" +#include "mongo/platform/unordered_set.h" +#include "mongo/util/mongoutils/str.h" + +namespace mongo { +namespace auth { + + /** + * Validates that the roles array described by rolesElement is valid. + * Also returns a new roles array (via the modifiedRolesArray output param) where any roles + * from the input array that were listed as strings have been expanded to a full role document. + */ + Status _validateAndModifyRolesArray(const BSONElement& rolesElement, + const std::string& dbname, + const AuthorizationManager* authzManager, + BSONArray* modifiedRolesArray) { + BSONArrayBuilder rolesBuilder; + + for (BSONObjIterator it(rolesElement.Obj()); it.more(); it.next()) { + BSONElement element = *it; + if (element.type() == String) { + RoleName roleName(element.String(), dbname); + if (!authzManager->roleExists(roleName)) { + return Status(ErrorCodes::RoleNotFound, + mongoutils::str::stream() << roleName.toString() << + " does not name an existing role"); + } + + rolesBuilder.append(BSON("name" << element.String() << + "source" << dbname << + "hasRole" << true << + "canDelegate" << false)); + } else if (element.type() == Object) { + // Check that the role object is valid + V2PrivilegeDocumentParser parser; + BSONObj roleObj = element.Obj(); + Status status = parser.checkValidRoleObject(roleObj); + if (!status.isOK()) { + return status; + } + + // Check that the role actually exists + std::string roleNameString; + std::string roleSource; + status = bsonExtractStringField(roleObj, "name", &roleNameString); + if (!status.isOK()) { + return status; + } + status = bsonExtractStringField(roleObj, "source", &roleSource); + if (!status.isOK()) { + return status; + } + + RoleName roleName(roleNameString, roleSource); + if (!authzManager->roleExists(roleName)) { + return Status(ErrorCodes::RoleNotFound, + mongoutils::str::stream() << roleName.toString() << + " does not name an existing role"); + } + + rolesBuilder.append(element); + } else { + return Status(ErrorCodes::UnsupportedFormat, + "Values in 'roles' array must be sub-documents or strings"); + } + } + + *modifiedRolesArray = rolesBuilder.arr(); + return Status::OK(); + } + + Status parseAndValidateCreateUserCommand(const BSONObj& cmdObj, + const std::string& dbname, + const AuthorizationManager* authzManager, + BSONObj* parsedUserObj) { + unordered_set<std::string> validFieldNames; + validFieldNames.insert("createUser"); + validFieldNames.insert("customData"); + validFieldNames.insert("pwd"); + validFieldNames.insert("roles"); + + // Iterate through all fields in command object and make sure there are no unexpected + // ones. + for (BSONObjIterator iter(cmdObj); iter.more(); iter.next()) { + StringData fieldName = (*iter).fieldNameStringData(); + if (!validFieldNames.count(fieldName.toString())) { + return Status(ErrorCodes::BadValue, + mongoutils::str::stream() << "\"" << fieldName << "\" is not " + "a valid argument to createUser"); + } + } + + BSONObjBuilder userObjBuilder; + userObjBuilder.append("_id", OID::gen()); + + // Parse user name + std::string userName; + Status status = bsonExtractStringField(cmdObj, "createUser", &userName); + if (!status.isOK()) { + return status; + } + + // Prevent creating users in the local database + if (dbname == "local") { + return Status(ErrorCodes::BadValue, "Cannot create users in the local database"); + } + + userObjBuilder.append(AuthorizationManager::USER_NAME_FIELD_NAME, userName); + userObjBuilder.append(AuthorizationManager::USER_SOURCE_FIELD_NAME, dbname); + + + // Parse password + if (cmdObj.hasField("pwd")) { + std::string clearTextPassword; + status = bsonExtractStringField(cmdObj, "pwd", &clearTextPassword); + if (!status.isOK()) { + return status; + } + + std::string password = auth::createPasswordDigest(userName, clearTextPassword); + userObjBuilder.append("credentials", BSON("MONGODB-CR" << password)); + } else { + if (dbname != "$external") { + return Status(ErrorCodes::BadValue, + "Must provide a 'pwd' field for all user documents, except those" + " with '$external' as the user's source"); + } + } + + + // Parse custom data + if (cmdObj.hasField("customData")) { + BSONElement element; + status = bsonExtractTypedField(cmdObj, "customData", Object, &element); + if (!status.isOK()) { + return status; + } + userObjBuilder.append("customData", element.Obj()); + } + + // Parse roles + if (cmdObj.hasField("roles")) { + BSONElement rolesElement; + status = bsonExtractTypedField(cmdObj, "roles", Array, &rolesElement); + if (!status.isOK()) { + return status; + } + BSONArray modifiedRolesArray; + status = _validateAndModifyRolesArray(rolesElement, + dbname, + authzManager, + &modifiedRolesArray); + if (!status.isOK()) { + return status; + } + + userObjBuilder.append("roles", modifiedRolesArray); + } + + *parsedUserObj = userObjBuilder.obj(); + + // Make sure document to insert is valid + V2PrivilegeDocumentParser parser; + status = parser.checkValidPrivilegeDocument(dbname, *parsedUserObj); + if (!status.isOK()) { + return status; + } + + return Status::OK(); + } + + + Status parseAndValidateUpdateUserCommand(const BSONObj& cmdObj, + const std::string& dbname, + const AuthorizationManager* authzManager, + BSONObj* parsedUpdateObj, + UserName* parsedUserName) { + unordered_set<std::string> validFieldNames; + validFieldNames.insert("updateUser"); + validFieldNames.insert("customData"); + validFieldNames.insert("pwd"); + validFieldNames.insert("roles"); + + // Iterate through all fields in command object and make sure there are no unexpected + // ones. + for (BSONObjIterator iter(cmdObj); iter.more(); iter.next()) { + StringData fieldName = (*iter).fieldNameStringData(); + if (!validFieldNames.count(fieldName.toString())) { + return Status(ErrorCodes::BadValue, + mongoutils::str::stream() << "\"" << fieldName << "\" is not " + "a valid argument to createUser"); + } + } + + BSONObjBuilder updateSetBuilder; + + // Parse user name + std::string userName; + Status status = bsonExtractStringField(cmdObj, "updateUser", &userName); + if (!status.isOK()) { + return status; + } + *parsedUserName = UserName(userName, dbname); + + // Parse password + if (cmdObj.hasField("pwd")) { + std::string clearTextPassword; + status = bsonExtractStringField(cmdObj, "pwd", &clearTextPassword); + if (!status.isOK()) { + return status; + } + + std::string password = auth::createPasswordDigest(userName, clearTextPassword); + updateSetBuilder.append("credentials.MONGODB-CR", password); + } + + + // Parse custom data + if (cmdObj.hasField("customData")) { + BSONElement element; + status = bsonExtractTypedField(cmdObj, "customData", Object, &element); + if (!status.isOK()) { + return status; + } + updateSetBuilder.append("customData", element.Obj()); + } + + // Parse roles + if (cmdObj.hasField("roles")) { + BSONElement rolesElement; + Status status = bsonExtractTypedField(cmdObj, "roles", Array, &rolesElement); + if (!status.isOK()) { + return status; + } + + BSONArray modifiedRolesObj; + status = _validateAndModifyRolesArray(rolesElement, + dbname, + authzManager, + &modifiedRolesObj); + if (!status.isOK()) { + return status; + } + + updateSetBuilder.append("roles", modifiedRolesObj); + } + + BSONObj updateSet = updateSetBuilder.obj(); + if (updateSet.isEmpty()) { + return Status(ErrorCodes::UserModificationFailed, + "Must specify at least one field to update in updateUser"); + } + + *parsedUpdateObj = BSON("$set" << updateSet); + return Status::OK(); + } + +} // namespace auth +} // namespace mongo diff --git a/src/mongo/db/commands/user_management_commands_parser.h b/src/mongo/db/commands/user_management_commands_parser.h new file mode 100644 index 00000000000..ce4b20b0118 --- /dev/null +++ b/src/mongo/db/commands/user_management_commands_parser.h @@ -0,0 +1,55 @@ +/** +* Copyright (C) 2013 10gen Inc. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License, version 3, +* as published by the Free Software Foundation. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <string> + +#include "mongo/base/status.h" +#include "mongo/base/disallow_copying.h" +#include "mongo/db/auth/user_name.h" +#include "mongo/db/jsobj.h" + +namespace mongo { + + class AuthorizationManager; + +namespace auth { + + /** + * Takes a command object describing an invocation of the "createUser" command on the database + * "dbname", and returns (via the output param "parsedUserObj") a user object that can be + * inserted into admin.system.users to create the user as described by the command object. + * Also validates the input and returns a non-ok Status if there is anything wrong. + */ + Status parseAndValidateCreateUserCommand(const BSONObj& cmdObj, + const std::string& dbname, + const AuthorizationManager* authzManager, + BSONObj* parsedUserObj); + + /** + * Takes a command object describing an invocation of the "updateUser" command on the database + * "dbname", and returns (via the output param "parsedUpdateObj") an update specifier that can + * be used to update the user document in admin.system.users as described by the command object, + * as well as the user name of the user being updated (via the "parsedUserName" output param). + * Also validates the input and returns a non-ok Status if there is anything wrong. + */ + Status parseAndValidateUpdateUserCommand(const BSONObj& cmdObj, + const std::string& dbname, + const AuthorizationManager* authzManager, + BSONObj* parsedUpdateObj, + UserName* parsedUserName); + +} // namespace auth +} // namespace mongo diff --git a/src/mongo/db/commands/user_management_commands_parser_test.cpp b/src/mongo/db/commands/user_management_commands_parser_test.cpp new file mode 100644 index 00000000000..62e20442506 --- /dev/null +++ b/src/mongo/db/commands/user_management_commands_parser_test.cpp @@ -0,0 +1,412 @@ +/* Copyright 2013 10gen Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Unit tests of the functions used for parsing the commands used for user management. + */ + +#include <vector> + +#include "mongo/base/status.h" +#include "mongo/client/auth_helpers.h" +#include "mongo/db/auth/authz_manager_external_state_mock.h" +#include "mongo/db/auth/authorization_manager.h" +#include "mongo/db/cmdline.h" +#include "mongo/db/commands/user_management_commands_parser.h" +#include "mongo/db/jsobj.h" +#include "mongo/unittest/unittest.h" + +namespace mongo { + + // Crutches to make the test compile + CmdLine cmdLine; + bool inShutdown() { + return false; + } + +namespace { + + class UserManagementCommandsParserTest : public ::mongo::unittest::Test { + public: + scoped_ptr<AuthorizationManager> authzManager; + AuthzManagerExternalStateMock* externalState; + void setUp() { + externalState = new AuthzManagerExternalStateMock(); + authzManager.reset(new AuthorizationManager(externalState)); + } + }; + + TEST_F(UserManagementCommandsParserTest, CreateUserCommandParsing) { + BSONArray emptyArray = BSONArrayBuilder().arr(); + BSONObj parsedUserObj; + + // Must have password + ASSERT_NOT_OK(auth::parseAndValidateCreateUserCommand(BSON("createUser" << "spencer" << + "roles" << emptyArray), + "test", + authzManager.get(), + &parsedUserObj)); + + // Must have roles array + ASSERT_NOT_OK(auth::parseAndValidateCreateUserCommand(BSON("createUser" << "spencer" << + "pwd" << "password"), + "test", + authzManager.get(), + &parsedUserObj)); + + // Cannot create users in the local db + ASSERT_NOT_OK(auth::parseAndValidateCreateUserCommand(BSON("createUser" << "spencer" << + "pwd" << "password" << + "roles" << emptyArray), + "local", + authzManager.get(), + &parsedUserObj)); + + // Cannot have extra fields + ASSERT_NOT_OK(auth::parseAndValidateCreateUserCommand(BSON("createUser" << "spencer" << + "pwd" << "password" << + "roles" << emptyArray << + "anotherField" << "garbage"), + "test", + authzManager.get(), + &parsedUserObj)); + + // Role must exist (string role) + ASSERT_NOT_OK(auth::parseAndValidateCreateUserCommand( + BSON("createUser" << "spencer" << + "pwd" << "password" << + "roles" << BSON_ARRAY("fakeRole")), + "test", + authzManager.get(), + &parsedUserObj)); + + // Role must exist (object role) + ASSERT_NOT_OK(auth::parseAndValidateCreateUserCommand( + BSON("createUser" << "spencer" << + "pwd" << "password" << + "roles" << BSON_ARRAY(BSON("name" << "fakeRole" << + "source" << "test" << + "hasRole" << true << + "canDelegate" << true))), + "test", + authzManager.get(), + &parsedUserObj)); + + // Role must have name + ASSERT_NOT_OK(auth::parseAndValidateCreateUserCommand( + BSON("createUser" << "spencer" << + "pwd" << "password" << + "roles" << BSON_ARRAY(BSON("source" << "test" << + "hasRole" << true << + "canDelegate" << true))), + "test", + authzManager.get(), + &parsedUserObj)); + + // Role must have source + ASSERT_NOT_OK(auth::parseAndValidateCreateUserCommand( + BSON("createUser" << "spencer" << + "pwd" << "password" << + "roles" << BSON_ARRAY(BSON("name" << "read" << + "hasRole" << true << + "canDelegate" << true))), + "test", + authzManager.get(), + &parsedUserObj)); + + // Role must have hasRole + ASSERT_NOT_OK(auth::parseAndValidateCreateUserCommand( + BSON("createUser" << "spencer" << + "pwd" << "password" << + "roles" << BSON_ARRAY(BSON("name" << "read" << + "source" << "test" << + "canDelegate" << true))), + "test", + authzManager.get(), + &parsedUserObj)); + + // Role must have canDelegate + ASSERT_NOT_OK(auth::parseAndValidateCreateUserCommand( + BSON("createUser" << "spencer" << + "pwd" << "password" << + "roles" << BSON_ARRAY(BSON("name" << "read" << + "source" << "test" << + "hasRole" << true))), + "test", + authzManager.get(), + &parsedUserObj)); + + // canDelegate and hasRole can't both be false + ASSERT_NOT_OK(auth::parseAndValidateCreateUserCommand( + BSON("createUser" << "spencer" << + "pwd" << "password" << + "roles" << BSON_ARRAY(BSON("name" << "read" << + "source" << "test" << + "hasRole" << false << + "canDelegate" << false))), + "test", + authzManager.get(), + &parsedUserObj)); + + // Empty roles array OK + ASSERT_OK(auth::parseAndValidateCreateUserCommand(BSON("createUser" << "spencer" << + "pwd" << "password" << + "roles" << emptyArray), + "test", + authzManager.get(), + &parsedUserObj)); + + + // Missing password OK if source is $external + ASSERT_OK(auth::parseAndValidateCreateUserCommand(BSON("createUser" << "spencer" << + "roles" << emptyArray), + "$external", + authzManager.get(), + &parsedUserObj)); + + // String role names OK + ASSERT_OK(auth::parseAndValidateCreateUserCommand( + BSON("createUser" << "spencer" << + "pwd" << "password" << + "roles" << BSON_ARRAY("read" << "dbAdmin")), + "test", + authzManager.get(), + &parsedUserObj)); + + ASSERT_EQUALS("spencer", parsedUserObj["name"].String()); + ASSERT_EQUALS("test", parsedUserObj["source"].String()); + std::string hashedPassword = auth::createPasswordDigest("spencer", "password"); + ASSERT_EQUALS(hashedPassword, parsedUserObj["credentials"].Obj()["MONGODB-CR"].String()); + std::vector<BSONElement> rolesArray = parsedUserObj["roles"].Array(); + ASSERT_EQUALS((size_t)2, rolesArray.size()); + ASSERT_EQUALS("read", rolesArray[0].Obj()["name"].String()); + ASSERT_EQUALS("test", rolesArray[0].Obj()["source"].String()); + ASSERT_EQUALS(true, rolesArray[0].Obj()["hasRole"].Bool()); + ASSERT_EQUALS(false, rolesArray[0].Obj()["canDelegate"].Bool()); + ASSERT_EQUALS("dbAdmin", rolesArray[1].Obj()["name"].String()); + ASSERT_EQUALS("test", rolesArray[1].Obj()["source"].String()); + ASSERT_EQUALS(true, rolesArray[1].Obj()["hasRole"].Bool()); + ASSERT_EQUALS(false, rolesArray[1].Obj()["canDelegate"].Bool()); + + + // Basic valid createUser command OK + ASSERT_OK(auth::parseAndValidateCreateUserCommand( + BSON("createUser" << "spencer" << + "pwd" << "password" << + "roles" << BSON_ARRAY(BSON("name" << "read" << + "source" << "test" << + "hasRole" << true << + "canDelegate" << false) << + BSON("name" << "dbAdminAnyDatabase" << + "source" << "admin" << + "hasRole" << false << + "canDelegate" << true)) << + "customData" << BSON("foo" << "bar")), + "test", + authzManager.get(), + &parsedUserObj)); + + ASSERT_EQUALS("spencer", parsedUserObj["name"].String()); + ASSERT_EQUALS("test", parsedUserObj["source"].String()); + hashedPassword = auth::createPasswordDigest("spencer", "password"); + ASSERT_EQUALS(hashedPassword, parsedUserObj["credentials"].Obj()["MONGODB-CR"].String()); + ASSERT_EQUALS("bar", parsedUserObj["customData"].Obj()["foo"].String()); + rolesArray = parsedUserObj["roles"].Array(); + ASSERT_EQUALS((size_t)2, rolesArray.size()); + ASSERT_EQUALS("read", rolesArray[0].Obj()["name"].String()); + ASSERT_EQUALS("test", rolesArray[0].Obj()["source"].String()); + ASSERT_EQUALS(true, rolesArray[0].Obj()["hasRole"].Bool()); + ASSERT_EQUALS(false, rolesArray[0].Obj()["canDelegate"].Bool()); + ASSERT_EQUALS("dbAdminAnyDatabase", rolesArray[1].Obj()["name"].String()); + ASSERT_EQUALS("admin", rolesArray[1].Obj()["source"].String()); + ASSERT_EQUALS(false, rolesArray[1].Obj()["hasRole"].Bool()); + ASSERT_EQUALS(true, rolesArray[1].Obj()["canDelegate"].Bool()); + + } + + + TEST_F(UserManagementCommandsParserTest, UpdateUserCommandParsing) { + BSONArray emptyArray = BSONArrayBuilder().arr(); + BSONObj parsedUpdateObj; + UserName parsedUserName; + + // Cannot have extra fields + ASSERT_NOT_OK(auth::parseAndValidateUpdateUserCommand(BSON("updateUser" << "spencer" << + "pwd" << "password" << + "roles" << emptyArray << + "anotherField" << "garbage"), + "test", + authzManager.get(), + &parsedUpdateObj, + &parsedUserName)); + + // Role must exist (string role) + ASSERT_NOT_OK(auth::parseAndValidateUpdateUserCommand( + BSON("updateUser" << "spencer" << + "pwd" << "password" << + "roles" << BSON_ARRAY("fakeRole")), + "test", + authzManager.get(), + &parsedUpdateObj, + &parsedUserName)); + + // Role must exist (object role) + ASSERT_NOT_OK(auth::parseAndValidateUpdateUserCommand( + BSON("updateUser" << "spencer" << + "pwd" << "password" << + "roles" << BSON_ARRAY(BSON("name" << "fakeRole" << + "source" << "test" << + "hasRole" << true << + "canDelegate" << true))), + "test", + authzManager.get(), + &parsedUpdateObj, + &parsedUserName)); + + // Role must have name + ASSERT_NOT_OK(auth::parseAndValidateUpdateUserCommand( + BSON("updateUser" << "spencer" << + "pwd" << "password" << + "roles" << BSON_ARRAY(BSON("source" << "test" << + "hasRole" << true << + "canDelegate" << true))), + "test", + authzManager.get(), + &parsedUpdateObj, + &parsedUserName)); + + // Role must have source + ASSERT_NOT_OK(auth::parseAndValidateUpdateUserCommand( + BSON("updateUser" << "spencer" << + "pwd" << "password" << + "roles" << BSON_ARRAY(BSON("name" << "read" << + "hasRole" << true << + "canDelegate" << true))), + "test", + authzManager.get(), + &parsedUpdateObj, + &parsedUserName)); + + // Role must have hasRole + ASSERT_NOT_OK(auth::parseAndValidateUpdateUserCommand( + BSON("updateUser" << "spencer" << + "pwd" << "password" << + "roles" << BSON_ARRAY(BSON("name" << "read" << + "source" << "test" << + "canDelegate" << true))), + "test", + authzManager.get(), + &parsedUpdateObj, + &parsedUserName)); + + // Role must have canDelegate + ASSERT_NOT_OK(auth::parseAndValidateUpdateUserCommand( + BSON("updateUser" << "spencer" << + "pwd" << "password" << + "roles" << BSON_ARRAY(BSON("name" << "read" << + "source" << "test" << + "hasRole" << true))), + "test", + authzManager.get(), + &parsedUpdateObj, + &parsedUserName)); + + // canDelegate and hasRole can't both be false + ASSERT_NOT_OK(auth::parseAndValidateUpdateUserCommand( + BSON("updateUser" << "spencer" << + "pwd" << "password" << + "roles" << BSON_ARRAY(BSON("name" << "read" << + "source" << "test" << + "hasRole" << false << + "canDelegate" << false))), + "test", + authzManager.get(), + &parsedUpdateObj, + &parsedUserName)); + + // Empty roles array OK + ASSERT_OK(auth::parseAndValidateUpdateUserCommand(BSON("updateUser" << "spencer" << + "pwd" << "password" << + "roles" << emptyArray), + "test", + authzManager.get(), + &parsedUpdateObj, + &parsedUserName)); + + // String role names OK + ASSERT_OK(auth::parseAndValidateUpdateUserCommand( + BSON("updateUser" << "spencer" << + "pwd" << "password" << + "roles" << BSON_ARRAY("read" << "dbAdmin")), + "test", + authzManager.get(), + &parsedUpdateObj, + &parsedUserName)); + + BSONObj setObj = parsedUpdateObj["$set"].Obj(); + + ASSERT_EQUALS(UserName("spencer", "test"), parsedUserName); + std::string hashedPassword = auth::createPasswordDigest("spencer", "password"); + ASSERT_EQUALS(hashedPassword, setObj["credentials.MONGODB-CR"].String()); + std::vector<BSONElement> rolesArray = setObj["roles"].Array(); + ASSERT_EQUALS((size_t)2, rolesArray.size()); + ASSERT_EQUALS("read", rolesArray[0].Obj()["name"].String()); + ASSERT_EQUALS("test", rolesArray[0].Obj()["source"].String()); + ASSERT_EQUALS(true, rolesArray[0].Obj()["hasRole"].Bool()); + ASSERT_EQUALS(false, rolesArray[0].Obj()["canDelegate"].Bool()); + ASSERT_EQUALS("dbAdmin", rolesArray[1].Obj()["name"].String()); + ASSERT_EQUALS("test", rolesArray[1].Obj()["source"].String()); + ASSERT_EQUALS(true, rolesArray[1].Obj()["hasRole"].Bool()); + ASSERT_EQUALS(false, rolesArray[1].Obj()["canDelegate"].Bool()); + + + // Basic valid updateUser command OK + ASSERT_OK(auth::parseAndValidateUpdateUserCommand( + BSON("updateUser" << "spencer" << + "pwd" << "password" << + "roles" << BSON_ARRAY(BSON("name" << "read" << + "source" << "test" << + "hasRole" << true << + "canDelegate" << false) << + BSON("name" << "dbAdminAnyDatabase" << + "source" << "admin" << + "hasRole" << false << + "canDelegate" << true)) << + "customData" << BSON("foo" << "bar")), + "test", + authzManager.get(), + &parsedUpdateObj, + &parsedUserName)); + + setObj = parsedUpdateObj["$set"].Obj(); + + ASSERT_EQUALS(UserName("spencer", "test"), parsedUserName); + hashedPassword = auth::createPasswordDigest("spencer", "password"); + ASSERT_EQUALS(hashedPassword, setObj["credentials.MONGODB-CR"].String()); + ASSERT_EQUALS("bar", setObj["customData"].Obj()["foo"].String()); + rolesArray = setObj["roles"].Array(); + ASSERT_EQUALS((size_t)2, rolesArray.size()); + ASSERT_EQUALS("read", rolesArray[0].Obj()["name"].String()); + ASSERT_EQUALS("test", rolesArray[0].Obj()["source"].String()); + ASSERT_EQUALS(true, rolesArray[0].Obj()["hasRole"].Bool()); + ASSERT_EQUALS(false, rolesArray[0].Obj()["canDelegate"].Bool()); + ASSERT_EQUALS("dbAdminAnyDatabase", rolesArray[1].Obj()["name"].String()); + ASSERT_EQUALS("admin", rolesArray[1].Obj()["source"].String()); + ASSERT_EQUALS(false, rolesArray[1].Obj()["hasRole"].Bool()); + ASSERT_EQUALS(true, rolesArray[1].Obj()["canDelegate"].Bool()); + } + +} // namespace +} // namespace mongo |