0
0
mirror of https://github.com/mongodb/mongo.git synced 2024-12-01 09:32:32 +01:00

SERVER-28190 Add internal tracking ids to users

This commit is contained in:
samantharitter 2017-03-27 13:49:41 -04:00
parent e6d9d9722c
commit 02edad4ea3
14 changed files with 202 additions and 41 deletions

View File

@ -85,6 +85,7 @@ MONGO_INITIALIZER_WITH_PREREQUISITES(SetupInternalSecurityUser, MONGO_NO_PREREQU
}
const std::string AuthorizationManager::USER_NAME_FIELD_NAME = "user";
const std::string AuthorizationManager::USER_ID_FIELD_NAME = "userId";
const std::string AuthorizationManager::USER_DB_FIELD_NAME = "db";
const std::string AuthorizationManager::ROLE_NAME_FIELD_NAME = "role";
const std::string AuthorizationManager::ROLE_DB_FIELD_NAME = "db";
@ -384,6 +385,7 @@ Status AuthorizationManager::_initializeUserFromPrivilegeDocument(User* user,
const BSONObj& privDoc) {
V2UserDocumentParser parser;
std::string userName = parser.extractUserNameFromUserDocument(privDoc);
if (userName != user->getName().getUser()) {
return Status(ErrorCodes::BadValue,
mongoutils::str::stream() << "User name from privilege document \""
@ -394,6 +396,8 @@ Status AuthorizationManager::_initializeUserFromPrivilegeDocument(User* user,
0);
}
user->setID(parser.extractUserIDFromUserDocument(privDoc));
Status status = parser.initializeUserCredentialsFromUserDocument(user, privDoc);
if (!status.isOK()) {
return status;
@ -444,9 +448,36 @@ Status AuthorizationManager::getRoleDescriptionsForDB(OperationContext* opCtx,
opCtx, dbname, privileges, showBuiltinRoles, result);
}
Status AuthorizationManager::acquireUser(OperationContext* opCtx,
const UserName& userName,
User** acquiredUser) {
Status AuthorizationManager::acquireUserToRefreshSessionCache(OperationContext* opCtx,
const UserName& userName,
boost::optional<OID> id,
User** acquiredUser) {
// Funnel down to acquireUserForInitialAuth, and then do the id checking.
auto status = acquireUserForInitialAuth(opCtx, userName, acquiredUser);
if (!status.isOK()) {
// If we got a non-ok status, no acquiredUser should be set, so we can simply
// return without doing an id check.
return status;
}
// Verify that the returned user matches the id that we were passed.
if (id != (*acquiredUser)->getID()) {
// If the generations don't match, then fail.
releaseUser(*acquiredUser);
*acquiredUser = nullptr;
return Status(ErrorCodes::UserNotFound,
mongoutils::str::stream() << "User id from privilege document \""
<< userName.toString()
<< "\" doesn't match provided user id",
0);
}
return status;
}
Status AuthorizationManager::acquireUserForInitialAuth(OperationContext* opCtx,
const UserName& userName,
User** acquiredUser) {
if (userName == internalSecurity.user->getName()) {
*acquiredUser = internalSecurity.user;
return Status::OK();
@ -513,6 +544,7 @@ Status AuthorizationManager::acquireUser(OperationContext* opCtx,
authzVersion = schemaVersionInvalid;
}
if (!status.isOK())
return status;

View File

@ -31,6 +31,8 @@
#include <memory>
#include <string>
#include <boost/optional.hpp>
#include "mongo/base/disallow_copying.h"
#include "mongo/base/status.h"
#include "mongo/bson/mutable/element.h"
@ -82,6 +84,7 @@ public:
~AuthorizationManager();
static const std::string USER_NAME_FIELD_NAME;
static const std::string USER_ID_FIELD_NAME;
static const std::string USER_DB_FIELD_NAME;
static const std::string ROLE_NAME_FIELD_NAME;
static const std::string ROLE_DB_FIELD_NAME;
@ -250,16 +253,41 @@ public:
std::vector<BSONObj>* result);
/**
* Returns the User object for the given userName in the out parameter "acquiredUser".
* If the user cache already has a user object for this user, it increments the refcount
* on that object and gives out a pointer to it. If no user object for this user name
* exists yet in the cache, reads the user's privilege document from disk, builds up
* a User object, sets the refcount to 1, and gives that out. The returned user may
* be invalid by the time the caller gets access to it.
* The AuthorizationManager retains ownership of the returned User object.
* On non-OK Status return values, acquiredUser will not be modified.
* Returns the User object for the given userName in the out parameter "acquiredUser".
*
* This method should be used only when initially authenticating a user, in contexts when
* the caller does not yet have an id for this user. When the caller already has access
* to a user document, acquireUserToRefreshSessionCache should be used instead.
*
* If no user object for this user name exists yet in the cache, read the user's privilege
* document from disk, build up a User object, sets the refcount to 1, and give that out.
*
* The returned user may be invalid by the time the caller gets access to it.
* The AuthorizationManager retains ownership of the returned User object.
* On non-OK Status return values, acquiredUser will not be modified.
*/
Status acquireUser(OperationContext* opCtx, const UserName& userName, User** acquiredUser);
Status acquireUserForInitialAuth(OperationContext* opCtx,
const UserName& userName,
User** acquiredUser);
/**
* Returns the User object for the given userName in the out parameter "acquiredUser".
*
* This method must be called with a user id (the unset optional, boost::none, will be
* understood as a distinct id for a pre-3.6 user). The acquired user must match
* both the given name and given id, or this method will return an error. This method
* should be used when the caller is refresing a user document they already have.
*
* If no user object for this user name exists yet in the cache, read the user's privilege
* document from disk, build up a User object, sets the refcount to 1, and give that out.
*
* The returned user may be invalid by the time the caller gets access to it.
* The AuthorizationManager retains ownership of the returned User object.
* On non-OK Status return values, acquiredUser will not be modified.
*/
Status acquireUserToRefreshSessionCache(OperationContext* opCtx,
const UserName& userName,
boost::optional<OID> id,
User** acquiredUser);
/**
* Decrements the refcount of the given User object. If the refcount has gone to zero,
@ -372,8 +400,8 @@ private:
/**
* Cached value of the authorization schema version.
*
* May be set by acquireUser() and getAuthorizationVersion(). Invalidated by
* invalidateUserCache().
* May be set by acquireUserForInitialAuth(), acquireUserToRefreshSessionCache(),
* and getAuthorizationVersion(). Invalidated by invalidateUserCache().
*
* Reads and writes guarded by CacheGuard.
*/

View File

@ -218,7 +218,7 @@ TEST_F(AuthorizationManagerTest, testAcquireV2User) {
BSONObj()));
User* v2read;
ASSERT_OK(authzManager->acquireUser(&opCtx, UserName("v2read", "test"), &v2read));
ASSERT_OK(authzManager->acquireUserForInitialAuth(&opCtx, UserName("v2read", "test"), &v2read));
ASSERT_EQUALS(UserName("v2read", "test"), v2read->getName());
ASSERT(v2read->isValid());
ASSERT_EQUALS(1U, v2read->getRefCount());
@ -232,7 +232,8 @@ TEST_F(AuthorizationManagerTest, testAcquireV2User) {
authzManager->releaseUser(v2read);
User* v2cluster;
ASSERT_OK(authzManager->acquireUser(&opCtx, UserName("v2cluster", "admin"), &v2cluster));
ASSERT_OK(authzManager->acquireUserForInitialAuth(
&opCtx, UserName("v2cluster", "admin"), &v2cluster));
ASSERT_EQUALS(UserName("v2cluster", "admin"), v2cluster->getName());
ASSERT(v2cluster->isValid());
ASSERT_EQUALS(1U, v2cluster->getRefCount());
@ -257,8 +258,8 @@ TEST_F(AuthorizationManagerTest, testLocalX509Authorization) {
ServiceContext::UniqueOperationContext opCtx = client->makeOperationContext();
User* x509User;
ASSERT_OK(
authzManager->acquireUser(opCtx.get(), UserName("CN=mongodb.com", "$external"), &x509User));
ASSERT_OK(authzManager->acquireUserForInitialAuth(
opCtx.get(), UserName("CN=mongodb.com", "$external"), &x509User));
ASSERT(x509User->isValid());
stdx::unordered_set<RoleName> expectedRoles{RoleName("read", "test"),
@ -291,8 +292,8 @@ TEST_F(AuthorizationManagerTest, testLocalX509AuthorizationInvalidUser) {
ServiceContext::UniqueOperationContext opCtx = client->makeOperationContext();
User* x509User;
ASSERT_NOT_OK(
authzManager->acquireUser(opCtx.get(), UserName("CN=10gen.com", "$external"), &x509User));
ASSERT_NOT_OK(authzManager->acquireUserForInitialAuth(
opCtx.get(), UserName("CN=10gen.com", "$external"), &x509User));
}
TEST_F(AuthorizationManagerTest, testLocalX509AuthenticationNoAuthorization) {
@ -304,8 +305,8 @@ TEST_F(AuthorizationManagerTest, testLocalX509AuthenticationNoAuthorization) {
ServiceContext::UniqueOperationContext opCtx = client->makeOperationContext();
User* x509User;
ASSERT_NOT_OK(
authzManager->acquireUser(opCtx.get(), UserName("CN=mongodb.com", "$external"), &x509User));
ASSERT_NOT_OK(authzManager->acquireUserForInitialAuth(
opCtx.get(), UserName("CN=mongodb.com", "$external"), &x509User));
}
/**
@ -403,7 +404,7 @@ TEST_F(AuthorizationManagerTest, testAcquireV2UserWithUnrecognizedActions) {
BSONObj()));
User* myUser;
ASSERT_OK(authzManager->acquireUser(&opCtx, UserName("myUser", "test"), &myUser));
ASSERT_OK(authzManager->acquireUserForInitialAuth(&opCtx, UserName("myUser", "test"), &myUser));
ASSERT_EQUALS(UserName("myUser", "test"), myUser->getName());
ASSERT(myUser->isValid());
ASSERT_EQUALS(1U, myUser->getRefCount());

View File

@ -104,7 +104,7 @@ void AuthorizationSession::startRequest(OperationContext* opCtx) {
Status AuthorizationSession::addAndAuthorizeUser(OperationContext* opCtx,
const UserName& userName) {
User* user;
Status status = getAuthorizationManager().acquireUser(opCtx, userName, &user);
Status status = getAuthorizationManager().acquireUserForInitialAuth(opCtx, userName, &user);
if (!status.isOK()) {
return status;
}
@ -753,7 +753,9 @@ void AuthorizationSession::_refreshUserInfoAsNeeded(OperationContext* opCtx) {
UserName name = user->getName();
User* updatedUser;
Status status = authMan.acquireUser(opCtx, name, &updatedUser);
Status status =
authMan.acquireUserToRefreshSessionCache(opCtx, name, user->getID(), &updatedUser);
switch (status.code()) {
case ErrorCodes::OK: {
// Success! Replace the old User object with the updated one.

View File

@ -301,9 +301,12 @@ protected:
private:
// If any users authenticated on this session are marked as invalid this updates them with
// up-to-date information. May require a read lock on the "admin" db to read the user data.
//
// When refreshing a user document, we will use the current user's id to confirm that our
// user is of the same generation as the refreshed user document. If the generations don't
// match we will remove the outdated user document from the cache.
void _refreshUserInfoAsNeeded(OperationContext* opCtx);
// Checks if this connection is authorized for the given Privilege, ignoring whether or not
// we should even be doing authorization checks in general. Note: this may acquire a read
// lock on the admin database (to update out-of-date user privilege information).

View File

@ -91,11 +91,12 @@ StatusWith<bool> SaslPLAINServerConversation::step(StringData inputData, std::st
User* userObj;
// The authentication database is also the source database for the user.
Status status =
_saslAuthSession->getAuthorizationSession()->getAuthorizationManager().acquireUser(
_saslAuthSession->getOpCtxt(),
UserName(_user, _saslAuthSession->getAuthenticationDatabase()),
&userObj);
Status status = _saslAuthSession->getAuthorizationSession()
->getAuthorizationManager()
.acquireUserForInitialAuth(
_saslAuthSession->getOpCtxt(),
UserName(_user, _saslAuthSession->getAuthenticationDatabase()),
&userObj);
if (!status.isOK()) {
return StatusWith<bool>(status);

View File

@ -164,9 +164,9 @@ StatusWith<bool> SaslSCRAMSHA1ServerConversation::_firstStep(std::vector<string>
// The authentication database is also the source database for the user.
User* userObj;
Status status =
_saslAuthSession->getAuthorizationSession()->getAuthorizationManager().acquireUser(
_saslAuthSession->getOpCtxt(), user, &userObj);
Status status = _saslAuthSession->getAuthorizationSession()
->getAuthorizationManager()
.acquireUserForInitialAuth(_saslAuthSession->getOpCtxt(), user, &userObj);
if (!status.isOK()) {
return StatusWith<bool>(status);

View File

@ -41,7 +41,7 @@
namespace mongo {
User::User(const UserName& name) : _name(name), _refCount(0), _isValid(1) {}
User::User(const UserName& name) : _name(name), _id(), _refCount(0), _isValid(1) {}
User::~User() {
dassert(_refCount == 0);
@ -51,6 +51,10 @@ const UserName& User::getName() const {
return _name;
}
const boost::optional<OID>& User::getID() const {
return _id;
}
RoleNameIterator User::getRoles() const {
return makeRoleNameIteratorForContainer(_roles);
}
@ -85,12 +89,17 @@ const ActionSet User::getActionsForResource(const ResourcePattern& resource) con
User* User::clone() const {
std::unique_ptr<User> result(new User(_name));
result->_id = _id;
result->_privileges = _privileges;
result->_roles = _roles;
result->_credentials = _credentials;
return result.release();
}
void User::setID(boost::optional<OID> id) {
_id = std::move(id);
}
void User::setCredentials(const CredentialData& credentials) {
_credentials = credentials;
}

View File

@ -31,6 +31,7 @@
#include <vector>
#include "mongo/base/disallow_copying.h"
#include "mongo/bson/oid.h"
#include "mongo/db/auth/privilege.h"
#include "mongo/db/auth/resource_pattern.h"
#include "mongo/db/auth/role_name.h"
@ -85,6 +86,11 @@ public:
*/
const UserName& getName() const;
/**
* Returns the user's id.
*/
const boost::optional<OID>& getID() const;
/**
* Returns an iterator over the names of the user's direct roles
*/
@ -137,6 +143,11 @@ public:
// Mutators below. Mutation functions should *only* be called by the AuthorizationManager
/**
* Set the id for this user.
*/
void setID(boost::optional<OID> id);
/**
* Sets this user's authentication credentials.
*/
@ -206,6 +217,12 @@ public:
private:
UserName _name;
// An id for this user. We use this to identify different "generations" of the same username
// (ie a user "Lily" is dropped and then added). This field is optional to facilitate the
// upgrade path from 3.4 to 3.6. When comparing User documents' generations, we consider
// an unset _id field to be a distinct value that will fail to compare to any other id value.
boost::optional<OID> _id;
// Maps resource name to privilege on that resource
ResourcePrivilegeMap _privileges;

View File

@ -223,6 +223,7 @@ Status _checkV2RolesArray(const BSONElement& rolesElement) {
Status V2UserDocumentParser::checkValidUserDocument(const BSONObj& doc) const {
BSONElement userElement = doc[AuthorizationManager::USER_NAME_FIELD_NAME];
BSONElement userIDElement = doc[AuthorizationManager::USER_ID_FIELD_NAME];
BSONElement userDBElement = doc[AuthorizationManager::USER_DB_FIELD_NAME];
BSONElement credentialsElement = doc[CREDENTIALS_FIELD_NAME];
BSONElement rolesElement = doc[ROLES_FIELD_NAME];
@ -233,6 +234,11 @@ Status V2UserDocumentParser::checkValidUserDocument(const BSONObj& doc) const {
if (userElement.valueStringData().empty())
return _badValue("User document needs 'user' field to be non-empty", 0);
// If we have an id field, make sure it is an OID
if (!userIDElement.eoo() && (userIDElement.type() != BSONType::jstOID)) {
return _badValue("User document 'userId' field must be an OID", 0);
}
// Validate the "db" element
if (userDBElement.type() != String || userDBElement.valueStringData().empty()) {
return _badValue("User document needs 'db' field to be a non-empty string", 0);
@ -300,6 +306,15 @@ std::string V2UserDocumentParser::extractUserNameFromUserDocument(const BSONObj&
return doc[AuthorizationManager::USER_NAME_FIELD_NAME].str();
}
boost::optional<OID> V2UserDocumentParser::extractUserIDFromUserDocument(const BSONObj& doc) const {
BSONElement e = doc[AuthorizationManager::USER_ID_FIELD_NAME];
if (e.type() == BSONType::EOO) {
return boost::optional<OID>();
}
return e.OID();
}
Status V2UserDocumentParser::initializeUserCredentialsFromUserDocument(
User* user, const BSONObj& privDoc) const {
User::CredentialData credentials;

View File

@ -68,6 +68,8 @@ public:
std::string extractUserNameFromUserDocument(const BSONObj& doc) const;
boost::optional<OID> extractUserIDFromUserDocument(const BSONObj& doc) const;
Status initializeUserCredentialsFromUserDocument(User* user, const BSONObj& privDoc) const;
Status initializeUserRolesFromUserDocument(const BSONObj& doc, User* user) const;

View File

@ -32,6 +32,7 @@
#include "mongo/platform/basic.h"
#include "mongo/base/status.h"
#include "mongo/bson/oid.h"
#include "mongo/db/auth/action_set.h"
#include "mongo/db/auth/action_type.h"
#include "mongo/db/auth/authorization_manager.h"
@ -255,6 +256,32 @@ TEST_F(V2UserDocumentParsing, V2DocumentValidation) {
<< "roles"
<< emptyArray)));
// Id field should be OID
ASSERT_OK(v2parser.checkValidUserDocument(BSON("user"
<< "spencer"
<< "userId"
<< OID::gen()
<< "db"
<< "test"
<< "credentials"
<< BSON("MONGODB-CR"
<< "a")
<< "roles"
<< emptyArray)));
// Non-OID id fields are rejected
ASSERT_NOT_OK(v2parser.checkValidUserDocument(BSON("user"
<< "spencer"
<< "userId"
<< "notAnOID"
<< "db"
<< "test"
<< "credentials"
<< BSON("MONGODB-CR"
<< "a")
<< "roles"
<< emptyArray)));
// Need source field
ASSERT_NOT_OK(v2parser.checkValidUserDocument(BSON("user"
<< "spencer"
@ -388,6 +415,25 @@ TEST_F(V2UserDocumentParsing, V2DocumentValidation) {
<< "dbA")))));
}
TEST_F(V2UserDocumentParsing, V2UserIDExtraction) {
OID oid = OID::gen();
// No id is present
ASSERT(!v2parser.extractUserIDFromUserDocument(BSON("user"
<< "sam"
<< "db"
<< "test")));
// Valid OID is present
auto res = v2parser.extractUserIDFromUserDocument(BSON("user"
<< "sam"
<< "userId"
<< oid
<< "db"
<< "test"));
ASSERT(res);
ASSERT(res == oid);
}
TEST_F(V2UserDocumentParsing, V2CredentialExtraction) {
// Old "pwd" field not valid
ASSERT_NOT_OK(v2parser.initializeUserCredentialsFromUserDocument(user.get(),

View File

@ -263,7 +263,8 @@ Status CmdAuthenticate::_authenticateCR(OperationContext* opCtx,
}
User* userObj;
Status status = getGlobalAuthorizationManager()->acquireUser(opCtx, user, &userObj);
Status status =
getGlobalAuthorizationManager()->acquireUserForInitialAuth(opCtx, user, &userObj);
if (!status.isOK()) {
// Failure to find the privilege document indicates no-such-user, a fact that we do not
// wish to reveal to the client. So, we return AuthenticationFailed rather than passing

View File

@ -39,6 +39,7 @@
#include "mongo/bson/mutable/algorithm.h"
#include "mongo/bson/mutable/document.h"
#include "mongo/bson/mutable/element.h"
#include "mongo/bson/oid.h"
#include "mongo/bson/util/bson_extract.h"
#include "mongo/client/dbclientinterface.h"
#include "mongo/config.h"
@ -141,7 +142,7 @@ Status getCurrentUserRoles(OperationContext* opCtx,
unordered_set<RoleName>* roles) {
User* user;
authzManager->invalidateUserByName(userName); // Need to make sure cache entry is up to date
Status status = authzManager->acquireUser(opCtx, userName, &user);
Status status = authzManager->acquireUserForInitialAuth(opCtx, userName, &user);
if (!status.isOK()) {
return status;
}
@ -668,6 +669,7 @@ public:
userObjBuilder.append(
"_id", str::stream() << args.userName.getDB() << "." << args.userName.getUser());
userObjBuilder.append(AuthorizationManager::USER_NAME_FIELD_NAME, args.userName.getUser());
userObjBuilder.append(AuthorizationManager::USER_ID_FIELD_NAME, OID::gen());
userObjBuilder.append(AuthorizationManager::USER_DB_FIELD_NAME, args.userName.getDB());
ServiceContext* serviceContext = opCtx->getClient()->getServiceContext();
@ -1197,7 +1199,8 @@ public:
BSONObjBuilder userWithoutCredentials(usersArrayBuilder.subobjStart());
for (BSONObjIterator it(userDetails); it.more();) {
BSONElement e = it.next();
if (e.fieldNameStringData() != "credentials")
if (e.fieldNameStringData() != "credentials" &&
e.fieldNameStringData() != AuthorizationManager::USER_ID_FIELD_NAME)
userWithoutCredentials.append(e);
}
userWithoutCredentials.doneFast();
@ -1224,16 +1227,17 @@ public:
// Order results by user field then db field, matching how UserNames are ordered
queryBuilder.append("orderby", BSON("user" << 1 << "db" << 1));
BSONObj projection;
BSONObjBuilder projection;
if (!args.showCredentials) {
projection = BSON("credentials" << 0);
projection.append(AuthorizationManager::USER_ID_FIELD_NAME, 0);
projection.append("credentials", 0);
}
const stdx::function<void(const BSONObj&)> function = stdx::bind(
appendBSONObjToBSONArrayBuilder, &usersArrayBuilder, stdx::placeholders::_1);
queryAuthzDocument(opCtx,
AuthorizationManager::usersCollectionNamespace,
queryBuilder.done(),
projection,
projection.done(),
function);
}
result.append("users", usersArrayBuilder.arr());