/**
*    Copyright (C) 2012 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/>.
*
*    As a special exception, the copyright holders give permission to link the
*    code of portions of this program with the OpenSSL library under certain
*    conditions as described in each individual source file and distribute
*    linked combinations including the program with the OpenSSL library. You
*    must comply with the GNU Affero General Public License in all respects for
*    all of the code used other than as permitted herein. If you modify file(s)
*    with this exception, you may extend this exception to your version of the
*    file(s), but you are not obligated to do so. If you do not wish to do so,
*    delete this exception statement from your version. If you delete this
*    exception statement from all source files in the program, then also delete
*    it in the license file.
*/

#include "mongo/db/auth/user_document_parser.h"

#include <string>

#include "mongo/base/init.h"
#include "mongo/base/status.h"
#include "mongo/db/auth/authorization_manager.h"
#include "mongo/db/auth/user.h"
#include "mongo/db/jsobj.h"
#include "mongo/db/namespace_string.h"
#include "mongo/util/mongoutils/str.h"

namespace mongo {

namespace {
    const std::string ADMIN_DBNAME = "admin";

    const std::string ROLES_FIELD_NAME = "roles";
    const std::string PRIVILEGES_FIELD_NAME = "inheritedPrivileges";
    const std::string OTHER_DB_ROLES_FIELD_NAME = "otherDBRoles";
    const std::string READONLY_FIELD_NAME = "readOnly";
    const std::string CREDENTIALS_FIELD_NAME = "credentials";
    const std::string ROLE_NAME_FIELD_NAME = "role";
    const std::string ROLE_SOURCE_FIELD_NAME = "db";
    const std::string MONGODB_CR_CREDENTIAL_FIELD_NAME = "MONGODB-CR";
    const std::string SCRAM_CREDENTIAL_FIELD_NAME = "SCRAM-SHA-1";
    const std::string MONGODB_EXTERNAL_CREDENTIAL_FIELD_NAME = "external";

    inline Status _badValue(const char* reason, int location) {
        return Status(ErrorCodes::BadValue, reason, location);
    }

    inline Status _badValue(const std::string& reason, int location) {
        return Status(ErrorCodes::BadValue, reason, location);
    }

    inline StringData makeStringDataFromBSONElement(const BSONElement& element) {
        return StringData(element.valuestr(), element.valuestrsize() - 1);
    }

    Status _checkV1RolesArray(const BSONElement& rolesElement) {
        if (rolesElement.type() != Array) {
            return _badValue("Role fields must be an array when present in system.users entries",
                             0);
        }
        for (BSONObjIterator iter(rolesElement.embeddedObject()); iter.more(); iter.next()) {
            BSONElement element = *iter;
            if (element.type() != String || makeStringDataFromBSONElement(element).empty()) {
                return _badValue("Roles must be non-empty strings.", 0);
            }
        }
        return Status::OK();
    }
}  // namespace

    std::string V1UserDocumentParser::extractUserNameFromUserDocument(
            const BSONObj& doc) const {
        return doc[AuthorizationManager::V1_USER_NAME_FIELD_NAME].str();
    }

    Status V1UserDocumentParser::initializeUserCredentialsFromUserDocument(
            User* user, const BSONObj& privDoc) const {
        User::CredentialData credentials;
        if (privDoc.hasField(AuthorizationManager::PASSWORD_FIELD_NAME)) {
            credentials.password = privDoc[AuthorizationManager::PASSWORD_FIELD_NAME].String();
            credentials.isExternal = false;
        }
        else if (privDoc.hasField(AuthorizationManager::V1_USER_SOURCE_FIELD_NAME)) {
            std::string userSource = privDoc[AuthorizationManager::V1_USER_SOURCE_FIELD_NAME].String();
            if (userSource != "$external") {
                return Status(ErrorCodes::UnsupportedFormat,
                              "Cannot extract credentials from user documents without a password "
                              "and with userSource != \"$external\"");
            } else {
                credentials.isExternal = true;
            }
        } 
        else {
            return Status(ErrorCodes::UnsupportedFormat,
                          "Invalid user document: must have one of \"pwd\" and \"userSource\"");
        }

        user->setCredentials(credentials);
        return Status::OK();
    }

    static void _initializeUserRolesFromV0UserDocument(
            User* user, const BSONObj& privDoc, const StringData& dbname) {
        bool readOnly = privDoc["readOnly"].trueValue();
        if (dbname == "admin") {
            if (readOnly) {
                user->addRole(RoleName(RoleGraph::BUILTIN_ROLE_V0_ADMIN_READ, "admin"));
            } else {
                user->addRole(RoleName(RoleGraph::BUILTIN_ROLE_V0_ADMIN_READ_WRITE, "admin"));
            }
        } else {
            if (readOnly) {
                user->addRole(RoleName(RoleGraph::BUILTIN_ROLE_V0_READ, dbname));
            } else {
                user->addRole(RoleName(RoleGraph::BUILTIN_ROLE_V0_READ_WRITE, dbname));
            }
        }
    }

    Status _initializeUserRolesFromV1RolesArray(User* user,
                                                const BSONElement& rolesElement,
                                                const StringData& dbname) {
        static const char privilegesTypeMismatchMessage[] =
                "Roles in V1 user documents must be enumerated in an array of strings.";

        if (rolesElement.type() != Array)
            return Status(ErrorCodes::TypeMismatch, privilegesTypeMismatchMessage);

        for (BSONObjIterator iter(rolesElement.embeddedObject()); iter.more(); iter.next()) {
            BSONElement roleElement = *iter;
            if (roleElement.type() != String)
                return Status(ErrorCodes::TypeMismatch, privilegesTypeMismatchMessage);

            user->addRole(RoleName(roleElement.String(), dbname));
        }
        return Status::OK();
    }

    static Status _initializeUserRolesFromV1UserDocument(
                User* user, const BSONObj& privDoc, const StringData& dbname) {

        if (!privDoc[READONLY_FIELD_NAME].eoo()) {
            return Status(ErrorCodes::UnsupportedFormat,
                          "User documents may not contain both \"readonly\" and "
                          "\"roles\" fields");
        }

        Status status = _initializeUserRolesFromV1RolesArray(user,
                                                             privDoc[ROLES_FIELD_NAME],
                                                             dbname);
        if (!status.isOK()) {
            return status;
        }

        // If "dbname" is the admin database, handle the otherDBPrivileges field, which
        // grants privileges on databases other than "dbname".
        BSONElement otherDbPrivileges = privDoc[OTHER_DB_ROLES_FIELD_NAME];
        if (dbname == ADMIN_DBNAME) {
            switch (otherDbPrivileges.type()) {
            case EOO:
                break;
            case Object: {
                for (BSONObjIterator iter(otherDbPrivileges.embeddedObject());
                     iter.more(); iter.next()) {

                    BSONElement rolesElement = *iter;
                    status = _initializeUserRolesFromV1RolesArray(user,
                                                                  rolesElement,
                                                                  rolesElement.fieldName());
                    if (!status.isOK())
                        return status;
                }
                break;
            }
            default:
                return Status(ErrorCodes::TypeMismatch,
                              "Field \"otherDBRoles\" must be an object, if present.");
            }
        }
        else if (!otherDbPrivileges.eoo()) {
            return Status(ErrorCodes::UnsupportedFormat,
                          "Only the admin database may contain a field called \"otherDBRoles\"");
        }

        return Status::OK();
    }

    Status V1UserDocumentParser::initializeUserRolesFromUserDocument(
            User* user, const BSONObj& privDoc, const StringData& dbname) const {
        if (!privDoc.hasField("roles")) {
            _initializeUserRolesFromV0UserDocument(user, privDoc, dbname);
        } else {
            return _initializeUserRolesFromV1UserDocument(user, privDoc, dbname);
        }
        return Status::OK();
    }


    Status _checkV2RolesArray(const BSONElement& rolesElement) {
        if (rolesElement.eoo()) {
            return _badValue("User document needs 'roles' field to be provided", 0);
        }
        if (rolesElement.type() != Array) {
            return _badValue("'roles' field must be an array", 0);
        }
        for (BSONObjIterator iter(rolesElement.embeddedObject()); iter.more(); iter.next()) {
            if ((*iter).type() != Object) {
                return _badValue("Elements in 'roles' array must objects", 0);
            }
            Status status = V2UserDocumentParser::checkValidRoleObject((*iter).Obj());
            if (!status.isOK())
                return status;
        }
        return Status::OK();
    }

    Status V2UserDocumentParser::checkValidUserDocument(const BSONObj& doc) const {
        BSONElement userElement = doc[AuthorizationManager::USER_NAME_FIELD_NAME];
        BSONElement userDBElement = doc[AuthorizationManager::USER_DB_FIELD_NAME];
        BSONElement credentialsElement = doc[CREDENTIALS_FIELD_NAME];
        BSONElement rolesElement = doc[ROLES_FIELD_NAME];

        // Validate the "user" element.
        if (userElement.type() != String)
            return _badValue("User document needs 'user' field to be a string", 0);
        if (makeStringDataFromBSONElement(userElement).empty())
            return _badValue("User document needs 'user' field to be non-empty", 0);

        // Validate the "db" element
        if (userDBElement.type() != String ||
                makeStringDataFromBSONElement(userDBElement).empty()) {
            return _badValue("User document needs 'db' field to be a non-empty string", 0);
        }
        StringData userDBStr = makeStringDataFromBSONElement(userDBElement);
        if (!NamespaceString::validDBName(userDBStr) && userDBStr != "$external") {
            return _badValue(mongoutils::str::stream() << "'" << userDBStr <<
                                     "' is not a valid value for the db field.",
                             0);
        }

        // Validate the "credentials" element
        if (credentialsElement.eoo()) {
            return _badValue("User document needs 'credentials' object",
                    0);
        }
        if (credentialsElement.type() != Object) {
            return _badValue("User document needs 'credentials' field to be an object", 0);
        }

        BSONObj credentialsObj = credentialsElement.Obj();
        if (credentialsObj.isEmpty()) {
            return _badValue("User document needs 'credentials' field to be a non-empty object",
                             0);
        }
        if (userDBStr == "$external") {
            BSONElement externalElement = credentialsObj[MONGODB_EXTERNAL_CREDENTIAL_FIELD_NAME];
            if (externalElement.eoo() || externalElement.type() != Bool ||
                    !externalElement.Bool()) {
                return _badValue("User documents for users defined on '$external' must have "
                        "'credentials' field set to {external: true}", 0);
            }
        } 
        else {
            BSONElement scramElement = credentialsObj[SCRAM_CREDENTIAL_FIELD_NAME];
            BSONElement mongoCRElement = credentialsObj[MONGODB_CR_CREDENTIAL_FIELD_NAME];
            
            if (!mongoCRElement.eoo()) {
                if (mongoCRElement.type() != String ||
                    makeStringDataFromBSONElement(mongoCRElement).empty()) {
                    return _badValue("MONGODB-CR credential must to be a non-empty string"
                                     ", if present", 0);
                }
            }
            else if (!scramElement.eoo()) {
                if (scramElement.type() != Object) {
                    return _badValue("SCRAM credential must be an object, if present", 0);
                }
            }
            else {
                return _badValue("User document must provide credentials for all "
                        "non-external users", 0);
            }
        }

        // Validate the "roles" element.
        Status status = _checkV2RolesArray(rolesElement);
        if (!status.isOK())
            return status;

        return Status::OK();
    }

    std::string V2UserDocumentParser::extractUserNameFromUserDocument(
            const BSONObj& doc) const {
        return doc[AuthorizationManager::USER_NAME_FIELD_NAME].str();
    }

    Status V2UserDocumentParser::initializeUserCredentialsFromUserDocument(
            User* user, const BSONObj& privDoc) const {
        User::CredentialData credentials;
        std::string userDB = privDoc[AuthorizationManager::USER_DB_FIELD_NAME].String();
        BSONElement credentialsElement = privDoc[CREDENTIALS_FIELD_NAME];
        if (!credentialsElement.eoo()) {
            if (credentialsElement.type() != Object) {
                return Status(ErrorCodes::UnsupportedFormat,
                              "'credentials' field in user documents must be an object");
            }
            if (userDB == "$external") {
                BSONElement externalCredentialElement =
                        credentialsElement.Obj()[MONGODB_EXTERNAL_CREDENTIAL_FIELD_NAME];
                if (!externalCredentialElement.eoo()) {
                    if (externalCredentialElement.type() != Bool ||
                            !externalCredentialElement.Bool()) {
                        return Status(ErrorCodes::UnsupportedFormat,
                                      "'external' field in credentials object must be set to true");
                    } else {
                        credentials.isExternal = true;
                    }
                } else {
                    return Status(ErrorCodes::UnsupportedFormat,
                                  "User documents defined on '$external' must provide set "
                                  "credentials to {external:true}");
                }
            } else {
                                       
                BSONElement scramElement =
                        credentialsElement.Obj()[SCRAM_CREDENTIAL_FIELD_NAME];
                BSONElement mongoCRCredentialElement =
                        credentialsElement.Obj()[MONGODB_CR_CREDENTIAL_FIELD_NAME];
                
                if (scramElement.eoo() && mongoCRCredentialElement.eoo()) {
                    return Status(ErrorCodes::UnsupportedFormat,
                                  "User documents must provide credentials for SCRAM-SHA-1 "
                                  "or MONGODB-CR authentication");
                }

                if (!scramElement.eoo()) {
                    // We are asserting rather then returning errors since these
                    // fields should have been prepopulated by the calling code.
                    credentials.scram.iterationCount = 
                        scramElement.Obj()["iterationCount"].numberInt();
                    uassert(17501, "Invalid or missing SCRAM iteration count", 
                            credentials.scram.iterationCount > 0);

                    credentials.scram.salt = 
                        scramElement.Obj()["salt"].str();
                    uassert(17502, "Missing SCRAM salt", 
                            !credentials.scram.salt.empty());

                    credentials.scram.serverKey = 
                        scramElement["serverKey"].str();
                    uassert(17503, "Missing SCRAM serverKey", 
                            !credentials.scram.serverKey.empty());
                    
                    credentials.scram.storedKey = 
                        scramElement["storedKey"].str();
                    uassert(17504, "Missing SCRAM storedKey", 
                            !credentials.scram.storedKey.empty());
                }
                
                if (!mongoCRCredentialElement.eoo()) {
                    if (mongoCRCredentialElement.type() != String ||
                            makeStringDataFromBSONElement(mongoCRCredentialElement).empty()) {
                        return Status(ErrorCodes::UnsupportedFormat,
                                      "MONGODB-CR credentials must be non-empty strings");
                    } else {
                        credentials.password = mongoCRCredentialElement.String();
                        if (credentials.password.empty()) {
                            return Status(ErrorCodes::UnsupportedFormat,
                                  "User documents must provide authentication credentials");
                        }
                    }
                }
                credentials.isExternal = false;
            }
        } else {
                return Status(ErrorCodes::UnsupportedFormat,
                              "Cannot extract credentials from user documents without a "
                              "'credentials' field");
        }

        user->setCredentials(credentials);
        return Status::OK();
    }

    static Status _extractRoleDocumentElements(
            const BSONObj& roleObject,
            BSONElement* roleNameElement,
            BSONElement* roleSourceElement) {

        *roleNameElement = roleObject[ROLE_NAME_FIELD_NAME];
        *roleSourceElement = roleObject[ROLE_SOURCE_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 db must be non-empty strings");
        }

        return Status::OK();
    }

    Status V2UserDocumentParser::checkValidRoleObject(const BSONObj& roleObject) {
        BSONElement roleNameElement;
        BSONElement roleSourceElement;
        return _extractRoleDocumentElements(
                roleObject,
                &roleNameElement,
                &roleSourceElement);
    }

    Status V2UserDocumentParser::parseRoleName(const BSONObj& roleObject, RoleName* result) {
        BSONElement roleNameElement;
        BSONElement roleSourceElement;
        Status status =  _extractRoleDocumentElements(
                roleObject,
                &roleNameElement,
                &roleSourceElement);
        if (!status.isOK())
            return status;
        *result = RoleName(roleNameElement.str(), roleSourceElement.str());
        return status;
    }

    Status V2UserDocumentParser::parseRoleVector(const BSONArray& rolesArray,
                                                 std::vector<RoleName>* result) {
        std::vector<RoleName> roles;
        for (BSONObjIterator it(rolesArray); it.more(); it.next()) {
            if ((*it).type() != Object) {
                return Status(ErrorCodes::TypeMismatch, "Roles must be objects.");
            }
            RoleName role;
            Status status = parseRoleName((*it).Obj(), &role);
            if (!status.isOK())
                return status;
            roles.push_back(role);
        }
        std::swap(*result, roles);
        return Status::OK();
    }

    Status V2UserDocumentParser::initializeUserRolesFromUserDocument(
            const BSONObj& privDoc, User* user) const {

        BSONElement rolesElement = privDoc[ROLES_FIELD_NAME];

        if (rolesElement.type() != Array) {
            return Status(ErrorCodes::UnsupportedFormat,
                          "User document needs 'roles' field to be an array");
        }

        std::vector<RoleName> roles;
        for (BSONObjIterator it(rolesElement.Obj()); it.more(); it.next()) {
            if ((*it).type() != Object) {
                return Status(ErrorCodes::UnsupportedFormat,
                              "User document needs values in 'roles' array to be a sub-documents");
            }
            BSONObj roleObject = (*it).Obj();

            RoleName role;
            Status status = parseRoleName(roleObject, &role);
            if (!status.isOK()) {
                return status;
            }
            roles.push_back(role);
        }
        user->setRoles(makeRoleNameIteratorForContainer(roles));
        return Status::OK();
    }

    Status V2UserDocumentParser::initializeUserPrivilegesFromUserDocument(const BSONObj& doc,
                                                                          User* user) const {
        BSONElement privilegesElement = doc[PRIVILEGES_FIELD_NAME];
        if (privilegesElement.eoo())
            return Status::OK();
        if (privilegesElement.type() != Array) {
            return Status(ErrorCodes::UnsupportedFormat,
                          "User document 'inheritedPrivileges' element must be Array if present.");
        }
        PrivilegeVector privileges;
        std::string errmsg;
        for (BSONObjIterator it(privilegesElement.Obj()); it.more(); it.next()) {
            if ((*it).type() != Object) {
                warning() << "Wrong type of element in inheritedPrivileges array for " <<
                        user->getName() << ": " << *it;
                continue;
            }
            Privilege privilege;
            ParsedPrivilege pp;
            if (!pp.parseBSON((*it).Obj(), &errmsg) ||
                !ParsedPrivilege::parsedPrivilegeToPrivilege(pp, &privilege, &errmsg)) {

                warning() << "Could not parse privilege element in user document for " <<
                    user->getName() << ": " << errmsg;
                continue;
            }
            privileges.push_back(privilege);
        }
        user->setPrivileges(privileges);
        return Status::OK();
    }

} // namespace mongo
