Source: server/storage/safestorage.js

/*globals requireJS*/
/*eslint-env node*/
/**
 * This class forwards function calls to the storage and in addition:
 *  - checks that input data is of correct format.
 *  - checks that users are authorized to access/change projects.
 *  - updates _users and _projects collections on delete/createProject.
 *
 * @module Server:SafeStorage
 * @author pmeijer / https://github.com/pmeijer
 */

'use strict';

var Q = require('q'),
    REGEXP = requireJS('common/regexp'),
    Storage = require('./storage'),
    UserProject = require('./userproject');

function check(cond, deferred, msg) {
    var rejected = false;
    if (!cond) {
        deferred.reject(new Error('Invalid argument, ' + msg));
        rejected = true;
    }

    return rejected;
}

/**
 *
 * @param database
 * @param logger
 * @param gmeConfig
 * @param gmeAuth
 * @constructor
 */
function SafeStorage(database, logger, gmeConfig, gmeAuth) {
    Storage.call(this, database, logger, gmeConfig);
    this.metadataStorage = gmeAuth.metadataStorage;
    this.authorizer = gmeAuth.authorizer;
}

// Inherit from Storage
SafeStorage.prototype = Object.create(Storage.prototype);
SafeStorage.prototype.constructor = SafeStorage;

/**
 * Returns and array of dictionaries for each project the user has at least read access to.
 * If branches is set, the returned array will be filtered based on if the projects really do exist as
 * collections on their own. If branches is not set, there is no guarantee that the returned projects
 * really exist.
 *
 * Authorization level: read access for each returned project.
 *
 * @param {object} data - input parameters
 * @param {boolean} [data.info] - include the info field from the _projects collection.
 * @param {boolean} [data.rights] - include users' authorization information for each project.
 * @param {boolean} [data.branches] - include a dictionary with all branches and their hash.
 * @param {boolean} [data.tags] - include a dictionary with all tags and their hash.
 * @param {boolean} [data.hooks] - include the dictionary with all hooks.
 * @param {string} [data.projectId] - if given will return only single matching project.
 * @param {string} [data.username=gmeConfig.authentication.guestAccount]
 * @param {function} [callback]
 * @returns {Promise} //TODO: jsdocify this
 */
SafeStorage.prototype.getProjects = function (data, callback) {
    var deferred = Q.defer(),
        self = this,
        projectAuthParams = {
            entityType: self.authorizer.ENTITY_TYPES.PROJECT,
        },
        rejected = false;

    rejected = check(data !== null && typeof data === 'object', deferred, 'data is not an object.') ||
        check(typeof data.info === 'undefined' || typeof data.info === 'boolean', deferred,
            'data.info is not a boolean.') ||
        check(typeof data.rights === 'undefined' || typeof data.rights === 'boolean', deferred,
            'data.rights is not a boolean.') ||
        check(typeof data.branches === 'undefined' || typeof data.branches === 'boolean', deferred,
            'data.branches is not a boolean.') ||
        check(typeof data.hooks === 'undefined' || typeof data.hooks === 'boolean', deferred,
            'data.hooks is not a boolean.') ||
        check(typeof data.projectId === 'undefined' || REGEXP.PROJECT.test(data.projectId), deferred,
            'data.projectId failed regexp: ' + data.projectId);

    if (Object.hasOwn(data, 'username')) {
        rejected = rejected || check(typeof data.username === 'string', deferred, 'data.username is not a string.');
    } else {
        data.username = this.gmeConfig.authentication.guestAccount;
    }

    if (rejected === false) {
        (data.projectId ? Q.all([this.metadataStorage.getProject(data.projectId)]) : this.metadataStorage.getProjects())
            .then(function (allProjects) {
                function getAuthorizedProjects(projectData) {
                    var projectDeferred = Q.defer();

                    self.authorizer.getAccessRights(data.username, projectData._id, projectAuthParams)
                        .then(function (accessRights) {
                            if (accessRights && accessRights.read === true) {
                                if (data.rights === true) {
                                    projectData.rights = accessRights;
                                }
                                if (!data.info) {
                                    delete projectData.info;
                                }
                                if (!data.hooks) {
                                    delete projectData.hooks;
                                }
                                projectDeferred.resolve(projectData);
                            } else {
                                projectDeferred.resolve();
                            }
                        })
                        .catch(projectDeferred.reject);

                    return projectDeferred.promise;
                }

                return Q.all(allProjects.map(getAuthorizedProjects));
            })
            .then(async function (projects) {
                const result = [];
                for (const project of projects) {
                    try {
                        if (!project) {
                            continue;
                        }

                        if (data.branches === true) {
                            project.branches = await Storage.prototype.getBranches.call(self, {projectId: project._id});
                        }

                        if (data.tags === true) {
                            project.tags = await Storage.prototype.getTags.call(self, {projectId: project._id});
                        }

                        result.push(project);
                    } catch (err) {
                        if (err.message.indexOf('Project does not exist') > -1) {
                            self.logger.error('Inconsistency: project exists in user "' + data.username +
                                '" and in _projects, but not as a collection on its own: ', project._id);
                            // Proceed with other projects..
                        } else {
                            deferred.reject(err);
                        }
                    }
                }

                deferred.resolve(result);
            })
            .catch(function (err) {
                deferred.reject(err);
            });
    }

    return deferred.promise.nodeify(callback);
};

/**
 * Deletes a project from the _projects collection, deletes the collection for the the project and removes
 * the rights from all users.
 *
 * Authorization level: delete access for project
 *
 * @param {object} data - input parameters
 * @param {string} data.projectId - identifier for project.
 * @param {string} [data.username=gmeConfig.authentication.guestAccount]
 * @param {function} [callback]
 * @returns {Promise} //TODO: jsdocify this
 */
SafeStorage.prototype.deleteProject = function (data, callback) {
    var deferred = Q.defer(),
        self = this,
        projectAuthParams = {
            entityType: self.authorizer.ENTITY_TYPES.PROJECT,
        },
        rejected = false,
        didExist;

    rejected = check(data !== null && typeof data === 'object', deferred, 'data is not an object.') ||
        check(typeof data.projectId === 'string', deferred, 'data.projectId is not a string.') ||
        check(REGEXP.PROJECT.test(data.projectId), deferred, 'data.projectId failed regexp: ' + data.projectId);

    if (Object.hasOwn(data, 'username')) {
        rejected = rejected || check(typeof data.username === 'string', deferred, 'data.username is not a string.');
    } else {
        data.username = this.gmeConfig.authentication.guestAccount;
    }

    if (rejected === false) {
        this.authorizer.getAccessRights(data.username, data.projectId, projectAuthParams)
            .then(function (projectAccess) {
                if (projectAccess && projectAccess.delete) {
                    return Storage.prototype.deleteProject.call(self, data);
                } else {
                    throw new Error('Not authorized to delete project [' + data.projectId + ']');
                }
            })
            .then(function (didExist_) {
                didExist = didExist_;
                return self.authorizer.setAccessRights(true, data.projectId,
                    {read: false, write: false, delete: false}, projectAuthParams);
            })
            .then(function () {
                return self.metadataStorage.deleteProject(data.projectId);
            })
            .then(function () {
                deferred.resolve(didExist);
            })
            .catch(function (err) {
                deferred.reject(err);
            });
    }

    return deferred.promise.nodeify(callback);
};

/**
 * Creates a project and assigns a projectId by concatenating the username and the provided project name.
 * The user with the given username becomes the owner of the project.
 *
 * Authorization level: canCreate
 *
 * @param {object} data - input parameters
 * @param {string} data.projectName - name of new project.
 * @param {string} [data.username=gmeConfig.authentication.guestAccount]
 * @param {string} [data.ownerId=data.username]
 * @param {string} [data.kind] - Category of project typically based on meta.
 * @param {function} [callback]
 * @returns {promise} //TODO: jsdocify this
 */
SafeStorage.prototype.createProject = function (data, callback) {
    var deferred = Q.defer(),
        self = this,
        userAuthParams = {
            entityType: self.authorizer.ENTITY_TYPES.USER
        },
        projectAuthParams = {
            entityType: self.authorizer.ENTITY_TYPES.PROJECT
        },
        rejected = false;

    rejected = check(data !== null && typeof data === 'object', deferred, 'data is not an object.') ||
        check(data.kind === null || typeof data.kind === 'undefined' || typeof data.kind === 'string', deferred,
            'data.kind is not a string: ' + data.kind) ||
        check(typeof data.projectName === 'string', deferred, 'data.projectName is not a string.') ||
        check(REGEXP.PROJECT_NAME.test(data.projectName), deferred,
            'data.projectName failed regexp: ' + data.projectName);

    if (Object.hasOwn(data, 'username')) {
        rejected = rejected || check(typeof data.username === 'string', deferred, 'data.username is not a string.');
    } else {
        data.username = this.gmeConfig.authentication.guestAccount;
    }

    if (data.ownerId) {
        rejected = rejected || check(typeof data.ownerId === 'string', deferred, 'data.ownerId is not a string.');
    } else {
        data.ownerId = data.username;
    }

    if (rejected === false) {
        this.authorizer.getAccessRights(data.username, data.ownerId, userAuthParams)
            .then(function (ownerRights) {
                var now = (new Date()).toISOString(),
                    info = {
                        createdAt: now,
                        viewedAt: now,
                        modifiedAt: now,
                        creator: data.username,
                        viewer: data.username,
                        modifier: data.username,
                        kind: data.kind
                    };

                if (ownerRights.write !== true) {
                    throw new Error('Not authorized to create new project for [' + data.ownerId + ']');
                }

                return self.metadataStorage.addProject(data.ownerId, data.projectName, info);
            })
            .then(function (projectId) {
                data.projectId = projectId;

                return self.authorizer.setAccessRights(data.ownerId, projectId, {
                    read: true,
                    write: true,
                    delete: true
                }, projectAuthParams);
            })
            .then(function () {
                // Add the default projectHooks
                var hookIds = Object.keys(self.gmeConfig.webhooks.defaults),
                    cnt = hookIds.length;

                function addHooks() {
                    var hookData;
                    if (cnt === 0) {
                        return;
                    } else {
                        cnt -= 1;
                        hookData = JSON.parse(JSON.stringify(self.gmeConfig.webhooks.defaults[hookIds[cnt]]));

                        if (typeof hookData.url === 'string') {
                            delete hookData.options;
                            return self.metadataStorage.addProjectHook(data.projectId, hookIds[cnt], hookData)
                                .then(function () {
                                    return addHooks();
                                });
                        } else {
                            self.logger.debug('Url not specified in default hook - will not add it to new project.');
                            return addHooks();
                        }
                    }
                }

                return addHooks();
            })
            .then(function () {
                return Storage.prototype.createProject.call(self, data);
            })
            .then(function (dbProject) {
                var project = new UserProject(dbProject, self, self.logger, self.gmeConfig);
                project.setUser(data.username);
                deferred.resolve(project);
            })
            .catch(function (err) {
                // TODO: Clean up appropriately when failure to add to model, user or projects database.
                deferred.reject(err);
            });
    }

    return deferred.promise.nodeify(callback);
};

/**
 *
 * Authorization level: canCreate and delete access for project
 * @param data
 * @param callback
 * @returns {*}
 */
SafeStorage.prototype.transferProject = function (data, callback) {
    var deferred = Q.defer(),
        self = this,
        userAuthParams = {
            entityType: self.authorizer.ENTITY_TYPES.USER
        },
        projectAuthParams = {
            entityType: self.authorizer.ENTITY_TYPES.PROJECT
        },
        rejected = false;

    rejected = check(data !== null && typeof data === 'object', deferred, 'data is not an object.') ||
        check(typeof data.projectId === 'string', deferred, 'data.projectId is not a string.') ||
        check(REGEXP.PROJECT.test(data.projectId), deferred, 'data.projectId failed regexp: ' + data.projectId) ||
        check(typeof data.newOwnerId === 'string', deferred, 'data.newOwnerId is not a string.');

    if (Object.hasOwn(data, 'username')) {
        rejected = rejected || check(typeof data.username === 'string', deferred, 'data.username is not a string.');
    } else {
        data.username = this.gmeConfig.authentication.guestAccount;
    }

    if (rejected === false) {
        self.authorizer.getAccessRights(data.username, data.projectId, projectAuthParams)
            .then(function (projectAccess) {
                if (projectAccess && projectAccess.delete) {
                    return self.authorizer.getAccessRights(data.username, data.newOwnerId, userAuthParams);
                } else {
                    throw new Error('Not authorized to delete project [' + data.projectId + ']');
                }
            })
            .then(function (ownerRights) {
                if (ownerRights && ownerRights.write !== true) {
                    throw new Error('Not authorized to transfer project to [' + data.newOwnerId + ']');
                }

                // Remove old and add new metadata for the project.
                return self.metadataStorage.transferProject(data.projectId, data.newOwnerId);
            })
            .then(function (newProjectId) {
                // Rename the project collection.
                data.newProjectId = newProjectId;
                return Storage.prototype.renameProject.call(self, data);
            })
            .then(function () {
                // Remove all previous project access rights.
                self.authorizer.setAccessRights(true, data.projectId, {
                    read: false,
                    write: false,
                    delete: false
                }, projectAuthParams);
            })
            .then(function () {
                // Add full project access rights to the new owner.
                return self.authorizer.setAccessRights(data.newOwnerId, data.newProjectId, {
                    read: true,
                    write: true,
                    delete: true
                }, projectAuthParams);
            })
            .then(function () {
                deferred.resolve(data.newProjectId);
            })
            .catch(function (err) {
                deferred.reject(err);
            });
    }

    return deferred.promise.nodeify(callback);
};

/**
 * Duplicates a project including all data-objects, commits, branches etc.
 *
 * Authorization level: canCreate and read access for project
 *
 * @param {object} data - input parameters
 * @param {string} data.projectId - id of existing project that will be duplicated.
 * @param {string} data.projectName - name of new project.
 * @param {string} [data.username=gmeConfig.authentication.guestAccount]
 * @param {string} [data.ownerId=data.username]
 * @param {function} [callback]
 * @returns {promise} //TODO: jsdocify this
 */
SafeStorage.prototype.duplicateProject = function (data, callback) {
    var deferred = Q.defer(),
        self = this,
        userAuthParams = {
            entityType: self.authorizer.ENTITY_TYPES.USER
        },
        projectAuthParams = {
            entityType: self.authorizer.ENTITY_TYPES.PROJECT
        },
        rejected = false;

    rejected = check(data !== null && typeof data === 'object', deferred, 'data is not an object.') ||
        check(typeof data.projectId === 'string', deferred, 'data.projectId is not a string.') ||
        check(REGEXP.PROJECT.test(data.projectId), deferred, 'data.projectId failed regexp: ' + data.projectId) ||
        check(typeof data.projectName === 'string', deferred, 'data.projectName is not a string.') ||
        check(REGEXP.PROJECT_NAME.test(data.projectName), deferred,
            'data.projectName failed regexp: ' + data.projectName);

    if (Object.hasOwn(data, 'username')) {
        rejected = rejected || check(typeof data.username === 'string', deferred, 'data.username is not a string.');
    } else {
        data.username = this.gmeConfig.authentication.guestAccount;
    }

    if (data.ownerId) {
        rejected = rejected || check(typeof data.ownerId === 'string', deferred, 'data.ownerId is not a string.');
    } else {
        data.ownerId = data.username;
    }

    if (self.gmeConfig.seedProjects.allowDuplication === false) {
        deferred.reject(new Error('gmeConfig.seedProjects.allowDuplication is set to false'));
        rejected = true;
    }

    if (rejected === false) {
        self.authorizer.getAccessRights(data.username, data.projectId, projectAuthParams)
            .then(function (projectAccess) {
                if (projectAccess && projectAccess.read) {
                    return Q.all([
                        self.authorizer.getAccessRights(data.username, data.ownerId, userAuthParams),
                        self.metadataStorage.getProject(data.projectId)
                    ]);
                } else {
                    throw new Error('Not authorized to read project [' + data.projectId + ']');
                }
            })
            .then(function (res) {
                var ownerRights = res[0],
                    prevProjectData = res[1],
                    now = (new Date()).toISOString(),
                    info = {
                        createdAt: now,
                        viewedAt: now,
                        modifiedAt: now,
                        creator: data.username,
                        viewer: data.username,
                        modifier: data.username,
                        kind: prevProjectData.info.kind
                    };

                if (ownerRights && ownerRights.write !== true) {
                    throw new Error('Not authorized to create project for [' + data.ownerId + ']');
                }

                return self.metadataStorage.duplicateProject(data.projectId, data.ownerId, data.projectName, info);
            })
            .then(function (newProjectId) {
                data.newProjectId = newProjectId;
                return self.authorizer.setAccessRights(data.ownerId, newProjectId, {
                    read: true,
                    write: true,
                    delete: true
                }, projectAuthParams);
            })
            .then(function () {
                return Storage.prototype.duplicateProject.call(self, data);
            })
            .then(function (dbProject) {
                var project = new UserProject(dbProject, self, self.logger, self.gmeConfig);
                project.setUser(data.username);
                deferred.resolve(project);
            })
            .catch(function (err) {
                // TODO: Clean up appropriately when failure to add to model, user or projects database.
                deferred.reject(err);
            });
    }

    return deferred.promise.nodeify(callback);
};

/**
 * Returns a dictionary with all the branches and their hashes within a project.
 * Example: {
 *   master: '#someHash',
 *   b1: '#someOtherHash'
 * }
 *
 * Authorization level: read access for project
 *
 * @param {object} data - input parameters
 * @param {string} data.projectId - identifier for project.
 * @param {string} [data.username=gmeConfig.authentication.guestAccount]
 * @param {function} [callback]
 * @returns {promise} //TODO: jsdocify this
 */
SafeStorage.prototype.getBranches = function (data, callback) {
    var deferred = Q.defer(),
        self = this,
        projectAuthParams = {
            entityType: self.authorizer.ENTITY_TYPES.PROJECT
        },
        rejected = false;

    rejected = check(data !== null && typeof data === 'object', deferred, 'data is not an object.') ||
        check(typeof data.projectId === 'string', deferred, 'data.projectId is not a string.') ||
        check(REGEXP.PROJECT.test(data.projectId), deferred, 'data.projectId failed regexp: ' + data.projectId);

    if (Object.hasOwn(data, 'username')) {
        rejected = rejected || check(typeof data.username === 'string', deferred, 'data.username is not a string.');
    } else {
        data.username = this.gmeConfig.authentication.guestAccount;
    }

    if (rejected === false) {
        self.authorizer.getAccessRights(data.username, data.projectId, projectAuthParams)
            .then(function (projectAccess) {
                if (projectAccess && projectAccess.read) {
                    return Storage.prototype.getBranches.call(self, data);
                } else {
                    throw new Error('Not authorized to read project [' + data.projectId + ']');
                }
            })
            .then(function (result) {
                deferred.resolve(result);
            })
            .catch(function (err) {
                deferred.reject(err);
            });
    }

    return deferred.promise.nodeify(callback);
};

/**
 * Returns an array of commits for a project ordered by their timestamp.
 *
 * Authorization level: read access for project
 *
 * @param {object} data - input parameters
 * @param {string} data.projectId - identifier for project.
 * @param {number} data.number - maximum number of commits to load.
 * @param {number|string} data.before - timestamp or commitHash to load history from. When number given it will load
 *  data.number of commits strictly before data.before, when commitHash is given it will return that commit too.
 * @param {string} [data.username=gmeConfig.authentication.guestAccount]
 * @param {function} [callback]
 * @returns {promise} //TODO: jsdocify this
 */
SafeStorage.prototype.getCommits = function (data, callback) {
    var deferred = Q.defer(),
        self = this,
        projectAuthParams = {
            entityType: self.authorizer.ENTITY_TYPES.PROJECT
        },
        rejected = false;

    rejected = check(data !== null && typeof data === 'object', deferred, 'data is not an object.') ||
        check(typeof data.projectId === 'string', deferred, 'data.projectId is not a string.') ||
        check(REGEXP.PROJECT.test(data.projectId), deferred, 'data.projectId failed regexp: ' + data.projectId) ||
        check(typeof data.before === 'number' || typeof data.before === 'string', deferred,
            'data.before is not a number nor string') ||
        check(typeof data.number === 'number', deferred, 'data.number is not a number');

    if (typeof data.before === 'string') {
        rejected = rejected || check(REGEXP.HASH.test(data.before), deferred,
            'data.before is not a number nor a valid hash.');
    }

    if (Object.hasOwn(data, 'username')) {
        rejected = rejected || check(typeof data.username === 'string', deferred, 'data.username is not a string.');
    } else {
        data.username = this.gmeConfig.authentication.guestAccount;
    }

    if (rejected === false) {
        self.authorizer.getAccessRights(data.username, data.projectId, projectAuthParams)
            .then(function (projectAccess) {
                if (projectAccess && projectAccess.read) {
                    return Storage.prototype.getCommits.call(self, data);
                } else {
                    throw new Error('Not authorized to read project [' + data.projectId + ']');
                }
            })
            .then(function (result) {
                deferred.resolve(result);
            })
            .catch(function (err) {
                deferred.reject(err);
            });
    }

    return deferred.promise.nodeify(callback);
};

/**
 * Returns an array of commits starting from either a branch(es) or commitHash(es).
 * They are ordered by the rules (applied in order)
 *  1. Descendants are always before their ancestors
 *  2. By their timestamp
 *
 * Authorization level: read access for project
 *
 * @param {object} data - input parameters
 * @param {string} data.projectId - identifier for project.
 * @param {number} data.number - maximum number of commits to load.
 * @param {string|string[]} data.start - BranchName or commitHash, or an array of such.
 * @param {string} [data.username=gmeConfig.authentication.guestAccount]
 * @param {function} [callback]
 * @returns {promise} //TODO: jsdocify this
 */
SafeStorage.prototype.getHistory = function (data, callback) {
    var deferred = Q.defer(),
        self = this,
        projectAuthParams = {
            entityType: self.authorizer.ENTITY_TYPES.PROJECT
        },
        rejected = false;

    rejected = check(data !== null && typeof data === 'object', deferred, 'data is not an object.') ||
        check(typeof data.projectId === 'string', deferred, 'data.projectId is not a string.') ||
        check(REGEXP.PROJECT.test(data.projectId), deferred, 'data.projectId failed regexp: ' + data.projectId) ||
        check(typeof data.start === 'string' || (typeof data.start === 'object' && data.start instanceof Array),
            deferred, 'data.start is not a string or array') ||
        check(typeof data.number === 'number', deferred, 'data.number is not a number');

    if (Object.hasOwn(data, 'username')) {
        rejected = rejected || check(typeof data.username === 'string', deferred, 'data.username is not a string.');
    } else {
        data.username = this.gmeConfig.authentication.guestAccount;
    }

    if (rejected === false) {
        self.authorizer.getAccessRights(data.username, data.projectId, projectAuthParams)
            .then(function (projectAccess) {
                if (projectAccess && projectAccess.read) {
                    return Storage.prototype.getHistory.call(self, data);
                } else {
                    throw new Error('Not authorized to read project [' + data.projectId + ']');
                }
            })
            .then(function (result) {
                deferred.resolve(result);
            })
            .catch(function (err) {
                deferred.reject(err);
            });
    }

    return deferred.promise.nodeify(callback);
};

/**
 * Returns the latest commit data for a branch within the project. (This is the same data that is provided during
 * a BRANCH_UPDATE event.)
 *
 * Example: {
 *   projectId: 'guest+TestProject',
 *   branchName: 'master',
 *   commitObject: {
 *     _id: '#someCommitHash',
 *     root: '#someNodeHash',
 *     parents: ['#someOtherCommitHash'],
 *     update: ['guest'],
 *     time: 1430169614741,
 *     message: 'createChild(/1/2)',
 *     type: 'commit'
 *   },
 *   coreObject: [{coreObj}, ..., {coreObj}],
 * }
 *
 * Authorization level: read access for project
 *
 * @param {object} data - input parameters
 * @param {string} data.projectId - identifier for project.
 * @param {string} data.branchName - name of the branch.
 * @param {string} [data.username=gmeConfig.authentication.guestAccount]
 * @param {function} [callback]
 * @returns {promise} //TODO: jsdocify this
 */
SafeStorage.prototype.getLatestCommitData = function (data, callback) {
    var deferred = Q.defer(),
        self = this,
        projectAuthParams = {
            entityType: self.authorizer.ENTITY_TYPES.PROJECT
        },
        rejected = false;

    rejected = check(data !== null && typeof data === 'object', deferred, 'data is not an object.') ||
        check(typeof data.projectId === 'string', deferred, 'data.projectId is not a string.') ||
        check(REGEXP.PROJECT.test(data.projectId), deferred, 'data.projectId failed regexp: ' + data.projectId) ||
        check(typeof data.branchName === 'string', deferred, 'data.branchName is not a string.') ||
        check(REGEXP.BRANCH.test(data.branchName), deferred, 'data.branchName failed regexp: ' + data.branchName);

    if (Object.hasOwn(data, 'username')) {
        rejected = rejected || check(typeof data.username === 'string', deferred, 'data.username is not a string.');
    } else {
        data.username = this.gmeConfig.authentication.guestAccount;
    }

    if (rejected === false) {
        self.authorizer.getAccessRights(data.username, data.projectId, projectAuthParams)
            .then(function (projectAccess) {
                if (projectAccess && projectAccess.read) {
                    return Storage.prototype.getLatestCommitData.call(self, data);
                } else {
                    throw new Error('Not authorized to read project [' + data.projectId + ']');
                }
            })
            .then(function (result) {
                deferred.resolve(result);
            })
            .catch(function (err) {
                deferred.reject(err);
            });
    }

    return deferred.promise.nodeify(callback);
};

/**
 * Authorization: write access for data.projectId
 * @param data
 * @param callback
 * @returns {*}
 */
SafeStorage.prototype.makeCommit = function (data, callback) {
    var deferred = Q.defer(),
        self = this,
        projectAuthParams = {
            entityType: self.authorizer.ENTITY_TYPES.PROJECT
        },
        rejected = false;

    rejected = check(data !== null && typeof data === 'object', deferred, 'data is not an object.') ||

        check(typeof data.projectId === 'string', deferred, 'data.projectId is not a string.') ||
        check(REGEXP.PROJECT.test(data.projectId), deferred, 'data.projectId failed regexp: ' + data.projectId) ||

        check(data.commitObject !== null && typeof data.commitObject === 'object', deferred,
            'data.commitObject not an object.') ||
        check(data.coreObjects !== null && typeof data.coreObjects === 'object', deferred,
            'data.coreObjects not an object.');

    // Checks when branchName is given and the branch will be updated
    if (rejected === false && typeof data.branchName !== 'undefined') {
        rejected = check(typeof data.branchName === 'string', deferred, 'data.branchName is not a string.') ||
            check(REGEXP.BRANCH.test(data.branchName), deferred, 'data.branchName failed regexp: ' + data.branchName) ||
            check(typeof data.commitObject._id === 'string', deferred, 'data.commitObject._id is not a string.') ||
            check(typeof data.commitObject.root === 'string', deferred, 'data.commitObject.root is not a string.') ||
            check(REGEXP.HASH.test(data.commitObject._id), deferred,
                'data.commitObject._id is not a valid hash: ' + data.commitObject._id) ||
            check(data.commitObject.parents instanceof Array, deferred,
                'data.commitObject.parents is not an array.') ||
            check(data.commitObject.parents.length === 0 || typeof data.commitObject.parents[0] === 'string', deferred,
                'data.commitObject.parents[0] is not a string.') ||
            check(
                data.commitObject.parents.length === 0 || data.commitObject.parents[0] === '' ||
                REGEXP.HASH.test(data.commitObject.parents[0]), deferred,
                'data.commitObject.parents[0] is not a valid hash: ' + data.commitObject.parents[0]) ||
            check(REGEXP.HASH.test(data.commitObject.root), deferred,
                'data.commitObject.root is not a valid hash: ' + data.commitObject.root);
        // Commits without coreObjects is valid now (the assumption is that the rootObject does exist.
        //check(typeof data.coreObjects[data.commitObject.root] === 'object', deferred,
        //    'data.coreObjects[data.commitObject.root] is not an object');

        if (typeof data.oldHash === 'string') {
            // Provide the possibility to refer to an oldHash explicitly rather than from the commitObj,
            // the is needed when e.g. undoing/redoing.
            check(data.oldHash === '' || REGEXP.HASH.test(data.oldHash), deferred,
                'data.oldHash is not a valid hash: ' + data.oldHash);
        }
    }

    if (Object.hasOwn(data, 'username')) {
        rejected = rejected || check(typeof data.username === 'string', deferred, 'data.username is not a string.');
    } else {
        data.username = this.gmeConfig.authentication.guestAccount;
    }

    if (rejected === false) {
        self.authorizer.getAccessRights(data.username, data.projectId, projectAuthParams)
            .then(function (projectAccess) {
                if (projectAccess && projectAccess.write) {
                    return Storage.prototype.makeCommit.call(self, data);
                } else {
                    throw new Error('Not authorized to write project [' + data.projectId + ']');
                }
            })
            .then(function (result) {
                deferred.resolve(result);
            })
            .catch(function (err) {
                deferred.reject(err);
            });
    }

    return deferred.promise.nodeify(callback);
};

/**
 * Authorization: write access for data.projectId
 * @param {object} data - input parameters
 * @param {string} data.projectId - identifier for project.
 * @param {string} data.fromCommit - starting point of the squash.
 * @param {string} data.toCommitOrBranch - branch or commit where the endpoint of squash can be found.
 * @param {string} [data.username=gmeConfig.authentication.guestAccount]
 * @param callback
 * @returns {*}
 */
SafeStorage.prototype.squashCommits = function (data, callback) {
    var deferred = Q.defer(),
        self = this,
        projectAuthParams = {
            entityType: self.authorizer.ENTITY_TYPES.PROJECT
        },
        rejected = false;

    rejected = check(data !== null && typeof data === 'object', deferred, 'data is not an object.') ||

        check(typeof data.projectId === 'string', deferred, 'data.projectId is not a string.') ||
        check(REGEXP.PROJECT.test(data.projectId), deferred, 'data.projectId failed regexp: ' + data.projectId) ||

        check(typeof data.fromCommit === 'string', deferred,
            'data.fromCommit not a string.') ||
        check(REGEXP.HASH.test(data.fromCommit), deferred, 'data.fromCommit failed regexp: ' + data.fromCommit) ||
        check(typeof data.toCommitOrBranch === 'string', deferred,
            'data.toCommitOrBranch not a string.');

    if (Object.hasOwn(data, 'username')) {
        rejected = rejected || check(typeof data.username === 'string', deferred, 'data.username is not a string.');
    } else {
        data.username = this.gmeConfig.authentication.guestAccount;
    }

    if (rejected === false) {
        self.authorizer.getAccessRights(data.username, data.projectId, projectAuthParams)
            .then(function (projectAccess) {
                if (projectAccess && projectAccess.write) {
                    return Storage.prototype.squashCommits.call(self, data);
                } else {
                    throw new Error('Not authorized to write project [' + data.projectId + ']');
                }
            })
            .then(function (result) {
                deferred.resolve(result);
            })
            .catch(function (err) {
                deferred.reject(err);
            });
    }

    return deferred.promise.nodeify(callback);
};

/**
 * Authorization: read access for data.projectId
 * @param data
 * @param callback
 * @returns {*}
 */
SafeStorage.prototype.getBranchHash = function (data, callback) {
    var deferred = Q.defer(),
        self = this,
        projectAuthParams = {
            entityType: self.authorizer.ENTITY_TYPES.PROJECT
        },
        rejected = false;

    rejected = check(data !== null && typeof data === 'object', deferred, 'data is not an object.') ||

        check(typeof data.projectId === 'string', deferred, 'data.projectId is not a string.') ||
        check(REGEXP.PROJECT.test(data.projectId), deferred, 'data.projectId failed regexp: ' + data.projectId) ||

        check(typeof data.branchName === 'string', deferred, 'data.branchName is not a string.') ||
        check(REGEXP.BRANCH.test(data.branchName), deferred, 'data.branchName failed regexp: ' + data.branchName);

    if (Object.hasOwn(data, 'username')) {
        rejected = rejected || check(typeof data.username === 'string', deferred, 'data.username is not a string.');
    } else {
        data.username = this.gmeConfig.authentication.guestAccount;
    }

    if (rejected === false) {
        self.authorizer.getAccessRights(data.username, data.projectId, projectAuthParams)
            .then(function (projectAccess) {
                if (projectAccess && projectAccess.read) {
                    return Storage.prototype.getBranchHash.call(self, data);
                } else {
                    throw new Error('Not authorized to read project [' + data.projectId + ']');
                }
            })
            .then(function (result) {
                deferred.resolve(result);
            })
            .catch(function (err) {
                deferred.reject(err);
            });
    }

    return deferred.promise.nodeify(callback);
};

/**
 * Authorization: write access for data.projectId
 * @param data
 * @param callback
 * @returns {*}
 */
SafeStorage.prototype.setBranchHash = function (data, callback) {
    var deferred = Q.defer(),
        self = this,
        projectAuthParams = {
            entityType: self.authorizer.ENTITY_TYPES.PROJECT
        },
        rejected = false;

    rejected = check(data !== null && typeof data === 'object', deferred, 'data is not an object.') ||

        check(typeof data.projectId === 'string', deferred, 'data.projectId is not a string.') ||
        check(REGEXP.PROJECT.test(data.projectId), deferred, 'data.projectId failed regexp: ' + data.projectId) ||

        check(typeof data.branchName === 'string', deferred, 'data.branchName is not a string.') ||
        check(REGEXP.BRANCH.test(data.branchName), deferred, 'data.branchName failed regexp: ' + data.branchName) ||

        check(typeof data.oldHash === 'string', deferred, 'data.oldHash is not a string.') ||
        check(data.oldHash === '' || REGEXP.HASH.test(data.oldHash), deferred,
            'data.oldHash is not a valid hash: ' + data.oldHash) ||
        check(typeof data.newHash === 'string', deferred, 'data.newHash is not a string.') ||
        check(data.newHash === '' || REGEXP.HASH.test(data.newHash), deferred,
            'data.newHash is not a valid hash: ' + data.newHash);

    if (Object.hasOwn(data, 'username')) {
        rejected = rejected || check(typeof data.username === 'string', deferred, 'data.username is not a string.');
    } else {
        data.username = this.gmeConfig.authentication.guestAccount;
    }

    if (rejected === false) {
        self.authorizer.getAccessRights(data.username, data.projectId, projectAuthParams)
            .then(function (projectAccess) {
                if (projectAccess && projectAccess.write) {
                    return Storage.prototype.setBranchHash.call(self, data);
                } else {
                    throw new Error('Not authorized to write project [' + data.projectId + ']');
                }
            })
            .then(function (result) {
                deferred.resolve(result);
            })
            .catch(function (err) {
                deferred.reject(err);
            });
    }

    return deferred.promise.nodeify(callback);
};

/**
 * Authorization: read access for data.projectId
 * @param data
 * @param callback
 * @returns {*}
 */
SafeStorage.prototype.getCommonAncestorCommit = function (data, callback) {
    var deferred = Q.defer(),
        self = this,
        projectAuthParams = {
            entityType: self.authorizer.ENTITY_TYPES.PROJECT
        },
        rejected = false;

    rejected = check(data !== null && typeof data === 'object', deferred, 'data is not an object.') ||

        check(typeof data.projectId === 'string', deferred, 'data.projectId is not a string.') ||
        check(REGEXP.PROJECT.test(data.projectId), deferred, 'data.projectId failed regexp: ' + data.projectId) ||

        check(typeof data.commitA === 'string', deferred, 'data.commitA is not a string.') ||
        check(data.commitA === '' || REGEXP.HASH.test(data.commitA), deferred,
            'data.commitA is not a valid hash: ' + data.commitA) ||
        check(typeof data.commitB === 'string', deferred, 'data.commitB is not a string.') ||
        check(data.commitB === '' || REGEXP.HASH.test(data.commitB), deferred,
            'data.commitB is not a valid hash: ' + data.commitB);

    if (Object.hasOwn(data, 'username')) {
        rejected = rejected || check(typeof data.username === 'string', deferred, 'data.username is not a string.');
    } else {
        data.username = this.gmeConfig.authentication.guestAccount;
    }

    if (rejected === false) {
        self.authorizer.getAccessRights(data.username, data.projectId, projectAuthParams)
            .then(function (projectAccess) {
                if (projectAccess && projectAccess.read) {
                    return Storage.prototype.getCommonAncestorCommit.call(self, data);
                } else {
                    throw new Error('Not authorized to read project [' + data.projectId + ']');
                }
            })
            .then(function (commonHash) {
                deferred.resolve(commonHash);
            })
            .catch(function (err) {
                deferred.reject(err);
            });
    }

    return deferred.promise.nodeify(callback);
};

/**
 * Authorization: write access for data.projectId
 * @param data
 * @param callback
 * @returns {*}
 */
SafeStorage.prototype.createBranch = function (data, callback) {
    var deferred = Q.defer(),
        self = this,
        projectAuthParams = {
            entityType: self.authorizer.ENTITY_TYPES.PROJECT
        },
        rejected = false;

    rejected = check(data !== null && typeof data === 'object', deferred, 'data is not an object.') ||

        check(typeof data.projectId === 'string', deferred, 'data.projectId is not a string.') ||
        check(REGEXP.PROJECT.test(data.projectId), deferred, 'data.projectId failed regexp: ' + data.projectId) ||

        check(typeof data.branchName === 'string', deferred, 'data.branchName is not a string.') ||
        check(REGEXP.BRANCH.test(data.branchName), deferred, 'data.branchName failed regexp: ' + data.branchName) ||

        check(typeof data.hash === 'string', deferred, 'data.hash is not a string.') ||
        check(data.hash === '' || REGEXP.HASH.test(data.hash), deferred,
            'data.hash is not a valid hash: ' + data.hash);

    data.oldHash = '';
    data.newHash = data.hash;

    if (Object.hasOwn(data, 'username')) {
        rejected = rejected || check(typeof data.username === 'string', deferred, 'data.username is not a string.');
    } else {
        data.username = this.gmeConfig.authentication.guestAccount;
    }

    if (rejected === false) {
        self.authorizer.getAccessRights(data.username, data.projectId, projectAuthParams)
            .then(function (projectAccess) {
                if (projectAccess && projectAccess.write) {
                    return Storage.prototype.setBranchHash.call(self, data);
                } else {
                    throw new Error('Not authorized to write project [' + data.projectId + ']');
                }
            })
            .then(function (result) {
                deferred.resolve(result);
            })
            .catch(function (err) {
                deferred.reject(err);
            });
    }

    return deferred.promise.nodeify(callback);
};

/**
 * Authorization: write access for data.projectId
 * @param data
 * @param callback
 * @returns {*}
 */
SafeStorage.prototype.deleteBranch = function (data, callback) {
    var deferred = Q.defer(),
        self = this,
        projectAuthParams = {
            entityType: self.authorizer.ENTITY_TYPES.PROJECT
        },
        rejected = false;

    rejected = check(data !== null && typeof data === 'object', deferred, 'data is not an object.') ||

        check(typeof data.projectId === 'string', deferred, 'data.projectId is not a string.') ||
        check(REGEXP.PROJECT.test(data.projectId), deferred, 'data.projectId failed regexp: ' + data.projectId) ||

        check(typeof data.branchName === 'string', deferred, 'data.branchName is not a string.') ||
        check(REGEXP.BRANCH.test(data.branchName), deferred, 'data.branchName failed regexp: ' + data.branchName);

    if (Object.hasOwn(data, 'username')) {
        rejected = rejected || check(typeof data.username === 'string', deferred, 'data.username is not a string.');
    } else {
        data.username = this.gmeConfig.authentication.guestAccount;
    }

    if (rejected === false) {
        self.authorizer.getAccessRights(data.username, data.projectId, projectAuthParams)
            .then(function (projectAccess) {
                if (projectAccess && projectAccess.write) {
                    return Storage.prototype.getBranchHash.call(self, data);
                } else {
                    throw new Error('Not authorized to write project [' + data.projectId + ']');
                }
            })
            .then(function (branchHash) {
                data.oldHash = branchHash;
                data.newHash = '';
                return Storage.prototype.setBranchHash.call(self, data);
            })
            .then(function (result) {
                deferred.resolve(result);
            })
            .catch(function (err) {
                deferred.reject(err);
            });
    }

    return deferred.promise.nodeify(callback);
};

/**
 * Authorization: write access for data.projectId
 * @param data
 * @param callback
 * @returns {*}
 */
SafeStorage.prototype.createTag = function (data, callback) {
    var deferred = Q.defer(),
        self = this,
        projectAuthParams = {
            entityType: self.authorizer.ENTITY_TYPES.PROJECT
        },
        rejected = false;

    if (!Object.hasOwn(data, 'commitHash') && Object.hasOwn(data, 'hash')) {
        data.commitHash = data.hash;
    }

    rejected = check(data !== null && typeof data === 'object', deferred, 'data is not an object.') ||

        check(typeof data.projectId === 'string', deferred, 'data.projectId is not a string.') ||
        check(REGEXP.PROJECT.test(data.projectId), deferred, 'data.projectId failed regexp: ' + data.projectId) ||

        check(typeof data.tagName === 'string', deferred, 'data.tagName is not a string.') ||
        check(REGEXP.TAG.test(data.tagName), deferred, 'data.tagName failed regexp: ' + data.tagName) ||

        check(typeof data.commitHash === 'string', deferred, 'data.commitHash is not a string.') ||
        check(data.commitHash === '' || REGEXP.HASH.test(data.commitHash), deferred,
            'data.commitHash is not a valid hash: ' + data.commitHash);

    if (Object.hasOwn(data, 'username')) {
        rejected = rejected || check(typeof data.username === 'string', deferred, 'data.username is not a string.');
    } else {
        data.username = this.gmeConfig.authentication.guestAccount;
    }

    if (rejected === false) {
        self.authorizer.getAccessRights(data.username, data.projectId, projectAuthParams)
            .then(function (projectAccess) {
                if (projectAccess && projectAccess.write) {
                    return Storage.prototype.createTag.call(self, data);
                } else {
                    throw new Error('Not authorized to write project [' + data.projectId + ']');
                }
            })
            .then(function () {
                deferred.resolve();
            })
            .catch(function (err) {
                deferred.reject(err);
            });
    }

    return deferred.promise.nodeify(callback);
};

/**
 * Authorization: delete access for data.projectId
 * @param data
 * @param callback
 * @returns {*}
 */
SafeStorage.prototype.deleteTag = function (data, callback) {
    var deferred = Q.defer(),
        self = this,
        projectAuthParams = {
            entityType: self.authorizer.ENTITY_TYPES.PROJECT
        },
        rejected = false;

    rejected = check(data !== null && typeof data === 'object', deferred, 'data is not an object.') ||

        check(typeof data.projectId === 'string', deferred, 'data.projectId is not a string.') ||
        check(REGEXP.PROJECT.test(data.projectId), deferred, 'data.projectId failed regexp: ' + data.projectId) ||

        check(typeof data.tagName === 'string', deferred, 'data.tagName is not a string.') ||
        check(REGEXP.TAG.test(data.tagName), deferred, 'data.tagName failed regexp: ' + data.tagName);

    if (Object.hasOwn(data, 'username')) {
        rejected = rejected || check(typeof data.username === 'string', deferred, 'data.username is not a string.');
    } else {
        data.username = this.gmeConfig.authentication.guestAccount;
    }

    if (rejected === false) {
        self.authorizer.getAccessRights(data.username, data.projectId, projectAuthParams)
            .then(function (projectAccess) {
                if (projectAccess && projectAccess.delete) {
                    return Storage.prototype.deleteTag.call(self, data);
                } else {
                    throw new Error('Not authorized to delete from project [' + data.projectId + ']');
                }
            })
            .then(function () {
                deferred.resolve();
            })
            .catch(function (err) {
                deferred.reject(err);
            });
    }

    return deferred.promise.nodeify(callback);
};

/**
 * Returns a dictionary with all the tags and their commitHashes within a project.
 * Example: {
 *   tag1: '#someHash',
 *   taggen: '#someOtherHash'
 * }
 *
 * Authorization level: read access for project
 *
 * @param {object} data - input parameters
 * @param {string} data.projectId - identifier for project.
 * @param {string} [data.username=gmeConfig.authentication.guestAccount]
 * @param {function} [callback]
 * @returns {*}
 */
SafeStorage.prototype.getTags = function (data, callback) {
    var deferred = Q.defer(),
        self = this,
        projectAuthParams = {
            entityType: self.authorizer.ENTITY_TYPES.PROJECT
        },
        rejected = false;

    rejected = check(data !== null && typeof data === 'object', deferred, 'data is not an object.') ||
        check(typeof data.projectId === 'string', deferred, 'data.projectId is not a string.') ||
        check(REGEXP.PROJECT.test(data.projectId), deferred, 'data.projectId failed regexp: ' + data.projectId);

    if (Object.hasOwn(data, 'username')) {
        rejected = rejected || check(typeof data.username === 'string', deferred, 'data.username is not a string.');
    } else {
        data.username = this.gmeConfig.authentication.guestAccount;
    }

    if (rejected === false) {
        self.authorizer.getAccessRights(data.username, data.projectId, projectAuthParams)
            .then(function (projectAccess) {
                if (projectAccess && projectAccess.read) {
                    return Storage.prototype.getTags.call(self, data);
                } else {
                    throw new Error('Not authorized to read project [' + data.projectId + ']');
                }
            })
            .then(function (result) {
                deferred.resolve(result);
            })
            .catch(function (err) {
                deferred.reject(err);
            });
    }

    return deferred.promise.nodeify(callback);
};

/**
 * Authorization: read access
 * @param data
 * @param callback
 * @returns {*}
 */
SafeStorage.prototype.openProject = function (data, callback) {
    var deferred = Q.defer(),
        userProject,
        self = this;

    self._getProject(data)
        .then(function (dbProject) {
            userProject = new UserProject(dbProject, self, self.logger, self.gmeConfig);
            userProject.setUser(data.username);
            deferred.resolve(userProject);
        })
        .catch(function (err) {
            deferred.reject(err);
        });

    return deferred.promise.nodeify(callback);
};

/**
 * Authorization: read access
 * @param data
 * @param callback
 * @returns {*}
 */
SafeStorage.prototype._getProject = function (data, callback) {
    var deferred = Q.defer(),
        self = this,
        projectAuthParams = {
            entityType: self.authorizer.ENTITY_TYPES.PROJECT
        },
        rejected = false;

    rejected = check(data !== null && typeof data === 'object', deferred, 'data is not an object.') ||
        check(typeof data.projectId === 'string', deferred, 'data.projectId is not a string.') ||
        check(REGEXP.PROJECT.test(data.projectId), deferred, 'data.projectId failed regexp: ' + data.projectId);

    if (Object.hasOwn(data, 'username')) {
        rejected = rejected || check(typeof data.username === 'string', deferred, 'data.username is not a string.');
    } else {
        data.username = this.gmeConfig.authentication.guestAccount;
    }

    if (rejected === false) {
        self.authorizer.getAccessRights(data.username, data.projectId, projectAuthParams)
            .then(function (projectAccess) {
                if (projectAccess && projectAccess.read) {
                    return Storage.prototype.openProject.call(self, data);
                } else {
                    throw new Error('Not authorized to read project [' + data.projectId + ']');
                }
            })
            .then(function (dbProject) {
                deferred.resolve(dbProject);
            })
            .catch(function (err) {
                deferred.reject(err);
            });
    }

    return deferred.promise.nodeify(callback);
};

/**
 * Authorization: TODO: read access for data.projectId
 * @param data
 * @param callback
 * @returns {*}
 */
SafeStorage.prototype.loadObjects = function (data, callback) {
    var deferred = Q.defer(),
        self = this,
        projectAuthParams = {
            entityType: self.authorizer.ENTITY_TYPES.PROJECT
        },
        rejected = false;

    rejected = check(data !== null && typeof data === 'object', deferred, 'data is not an object.') ||
        check(typeof data.projectId === 'string', deferred, 'data.projectId is not a string.') ||
        check(REGEXP.PROJECT.test(data.projectId), deferred, 'data.projectId failed regexp: ' + data.projectId) ||
        check(data.hashes instanceof Array, deferred, 'data.hashes is not an array: ' + JSON.stringify(data.hashes));

    if (Object.hasOwn(data, 'username')) {
        rejected = rejected || check(typeof data.username === 'string', deferred, 'data.username is not a string.');
    } else {
        data.username = this.gmeConfig.authentication.guestAccount;
    }

    self.logger.debug('loadObjects', {metadata: data});
    if (rejected === false) {
        self.authorizer.getAccessRights(data.username, data.projectId, projectAuthParams)
            .then(function (projectAccess) {
                if (projectAccess && projectAccess.read) {
                    return Storage.prototype.loadObjects.call(self, data);
                } else {
                    throw new Error('Not authorized to read project [' + data.projectId + ']');
                }
            })
            .then(function (project) {
                deferred.resolve(project);
            })
            .catch(function (err) {
                deferred.reject(err);
            });
    }

    return deferred.promise.nodeify(callback);
};

/**
 * Returns a dictionary with all the hashes needed to load the containment of the input paths.
 *
 * Authorization level: read access for data.projectId
 *
 * @param {object} data - input parameters.
 * @param {string} data.projectId - identifier for project.
 * @param {string} [data.username=gmeConfig.authentication.guestAccount]
 * @param {object[]} data.pathsInfo - list of objects with parentHash and path.
 * @param {string[]} [data.excludes] - list of known object hashes that should not be returned.
 * @param {boolean} [data.excludeParents] - if true will only return the data for the node at the path.
 * @param {function} [callback]
 * @returns {promise} //TODO: jsdocify this
 */
SafeStorage.prototype.loadPaths = function (data, callback) {
    var deferred = Q.defer(),
        self = this,
        projectAuthParams = {
            entityType: self.authorizer.ENTITY_TYPES.PROJECT
        },
        rejected = false;

    rejected = check(data !== null && typeof data === 'object', deferred, 'data is not an object.') ||
        check(typeof data.projectId === 'string', deferred, 'data.projectId is not a string.') ||
        check(REGEXP.PROJECT.test(data.projectId), deferred, 'data.projectId failed regexp: ' + data.projectId) ||
        check(data.pathsInfo instanceof Array, deferred,
            'data.pathsInfo is not an array: ' + JSON.stringify(data.hashes));

    if (Object.hasOwn(data, 'username')) {
        rejected = rejected || check(typeof data.username === 'string', deferred, 'data.username is not a string.');
    } else {
        data.username = this.gmeConfig.authentication.guestAccount;
    }

    self.logger.debug('loadPaths', {metadata: data});
    if (rejected === false) {
        self.authorizer.getAccessRights(data.username, data.projectId, projectAuthParams)
            .then(function (projectAccess) {
                if (projectAccess && projectAccess.read) {
                    return Storage.prototype.loadPaths.call(self, data);
                } else {
                    throw new Error('Not authorized to read project [' + data.projectId + ']');
                }
            })
            .then(function (project) {
                deferred.resolve(project);
            })
            .catch(function (err) {
                deferred.reject(err);
            });
    }

    return deferred.promise.nodeify(callback);
};

SafeStorage.prototype.traverse = function (data, callback) {
    var deferred = Q.defer(),
        self = this,
        rejected = false;

    rejected = check(data !== null && typeof data === 'object', deferred, 'data is not an object.') ||
        check(typeof data.projectId === 'string', deferred, 'data.projectId is not a string.') ||
        check(REGEXP.PROJECT.test(data.projectId), deferred, 'data.projectId failed regexp: ' + data.projectId) ||
        check(typeof data.visitFn === 'function', deferred,
            'data.visitFn is not a function');

    if (Object.hasOwn(data, 'username')) {
        rejected = rejected || check(typeof data.username === 'string', deferred, 'data.username is not a string.');
    } else {
        data.username = this.gmeConfig.authentication.guestAccount;
    }

    self.logger.debug('traverse', {metadata: data});
    if (rejected === false) {
        return Storage.prototype.traverse.call(self, data);
    }

    return deferred.promise.nodeify(callback);
};

module.exports = SafeStorage;